{"id":306124,"date":"2026-05-12T13:22:31","date_gmt":"2026-05-12T13:22:31","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/abilityguard\/"},"modified":"2026-05-12T14:05:33","modified_gmt":"2026-05-12T14:05:33","slug":"abilityguard-mcp","status":"publish","type":"plugin","link":"https:\/\/wordpress.org\/plugins\/abilityguard-mcp\/","author":18136256,"comment_status":"closed","ping_status":"closed","template":"","meta":{"version":"1.3.5","stable_tag":"1.3.5","tested":"6.9.4","requires":"6.9","requires_php":"8.1","requires_plugins":null,"header_name":"Tessera for the Abilities API","header_author":"Ibrahim Hajjaj","header_description":"Snapshot, audit, and rollback layer for plugins that register abilities via the WordPress Abilities API.","assets_banners_color":"745835","last_updated":"2026-05-12 14:05:33","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/github.com\/ibrahimhajjaj\/abilityguard","header_author_uri":"https:\/\/github.com\/ibrahimhajjaj","rating":0,"author_block_rating":0,"active_installs":0,"downloads":30,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.3.4":{"tag":"1.3.4","author":"ibrahimhajjaj","date":"2026-05-12 13:22:00"},"1.3.5":{"tag":"1.3.5","author":"ibrahimhajjaj","date":"2026-05-12 14:05:33"}},"upgrade_notice":[],"ratings":[],"assets_icons":{"icon-256x256.png":{"filename":"icon-256x256.png","revision":3530025,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3530025,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":500}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.3.4","1.3.5"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3530025,"resolution":"1","location":"assets","locale":"","width":1440,"height":900},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3530025,"resolution":"2","location":"assets","locale":"","width":1440,"height":900},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3530025,"resolution":"3","location":"assets","locale":"","width":1440,"height":900},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3530025,"resolution":"4","location":"assets","locale":"","width":1440,"height":900},"screenshot-5.png":{"filename":"screenshot-5.png","revision":3530025,"resolution":"5","location":"assets","locale":"","width":1440,"height":900},"screenshot-6.png":{"filename":"screenshot-6.png","revision":3530025,"resolution":"6","location":"assets","locale":"","width":1440,"height":900},"screenshot-7.png":{"filename":"screenshot-7.png","revision":3530025,"resolution":"7","location":"assets","locale":"","width":1440,"height":900}},"screenshots":{"1":"Invocation timeline. Every ability call across REST, MCP, internal PHP, and WP-CLI, with caller attribution and per-row status.","2":"Approvals queue. Pending requests waiting on a human, with the requesting context and a one-click approve or reject.","3":"Invocation detail after a one-click rollback restored the captured pre-state.","4":"Search-as-you-type in the log: ability name, caller, status.","5":"Invocation detail, result tab, with redacted secret values restored on display when the encryption key is present.","6":"Snapshot drawer showing the captured pre-state and post-state for a destructive invocation.","7":"Multi-stage approval chain with per-stage capability and role routing."},"jetpack_post_was_ever_published":false},"plugin_section":[262246],"plugin_tags":[256265,8533,242115,10708,13261],"plugin_category":[],"plugin_contributors":[262678],"plugin_business_model":[],"class_list":["post-306124","plugin","type-plugin","status-publish","hentry","plugin_section-dashboard-widgets","plugin_tags-abilities-api","plugin_tags-audit","plugin_tags-mcp","plugin_tags-rollback","plugin_tags-safety","plugin_contributors-ibrahimhajjaj","plugin_committers-ibrahimhajjaj"],"banners":[],"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/icon-256x256.png?rev=3530025","icon_2x":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/icon-256x256.png?rev=3530025","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-1.png?rev=3530025","caption":"Invocation timeline. Every ability call across REST, MCP, internal PHP, and WP-CLI, with caller attribution and per-row status."},{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-2.png?rev=3530025","caption":"Approvals queue. Pending requests waiting on a human, with the requesting context and a one-click approve or reject."},{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-3.png?rev=3530025","caption":"Invocation detail after a one-click rollback restored the captured pre-state."},{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-4.png?rev=3530025","caption":"Search-as-you-type in the log: ability name, caller, status."},{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-5.png?rev=3530025","caption":"Invocation detail, result tab, with redacted secret values restored on display when the encryption key is present."},{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-6.png?rev=3530025","caption":"Snapshot drawer showing the captured pre-state and post-state for a destructive invocation."},{"src":"https:\/\/ps.w.org\/abilityguard-mcp\/assets\/screenshot-7.png?rev=3530025","caption":"Multi-stage approval chain with per-stage capability and role routing."}],"raw_content":"<!--section=description-->\n<p>Tessera is a developer library for plugin authors who register abilities via <code>wp_register_ability()<\/code> and want snapshot capture, audit logging, approval workflows, and one-click rollback for every invocation across REST, MCP, internal PHP, and WP-CLI without building it themselves.<\/p>\n\n<p>Declare what state your ability touches; Tessera handles the safety wrapper.<\/p>\n\n<h4>What you get out of the box<\/h4>\n\n<ul>\n<li><strong>Pre + post snapshots.<\/strong> Every safety-enabled invocation captures declared state before the callback and (on success) after, so the audit log can show a real diff.<\/li>\n<li><strong>Audit log.<\/strong> One row per invocation with ability name, caller (REST\/MCP\/CLI\/internal), user, args, result, status, duration, pre\/post hashes, and parent_invocation_id for nested calls.<\/li>\n<li><strong>One-click rollback.<\/strong> Restore captured state from post_meta, options, taxonomy term assignments, user roles + caps. File contents support tiered drift detection (mtime \/ mtime_size \/ critical_hash \/ full_hash) plus opt-in real byte-level rollback via <code>full_content<\/code> strategy.<\/li>\n<li><strong>Drift check on rollback.<\/strong> Live state is hashed and compared to the snapshot's post-state before restoring; if they differ the rollback returns an error unless forced.<\/li>\n<li><strong>Concurrency lock.<\/strong> Capture + execute is serialised per surface set via a MySQL advisory lock so two simultaneous invocations do not capture each other's mid-states.<\/li>\n<li><strong>Encrypted redaction.<\/strong> Scrub secrets out of args, results, and snapshots. Stores redacted values as AES-256-GCM envelopes so rollback can still restore them.<\/li>\n<li><strong>Approval queue.<\/strong> When <code>safety.requires_approval<\/code> is set, the wrapper blocks execution and returns a 202 pending response. A human approves or rejects via wp-admin, WP-CLI, or REST. Multi-stage sequential or parallel approval chains are supported.<\/li>\n<li><strong>Multisite support.<\/strong> Each subsite gets its own set of <code>wp_&lt;N&gt;_abilityguard_*<\/code> tables, with auto-install on <code>wp_initialize_site<\/code> and auto-drop on <code>wpmu_drop_tables<\/code>.<\/li>\n<li><strong>Retention.<\/strong> Daily WP-Cron prunes old log rows (defaults: 30 days normal, 180 days destructive) and orphaned snapshots.<\/li>\n<\/ul>\n\n<h4>Surfaces<\/h4>\n\n<ul>\n<li><strong>PHP API<\/strong> with <code>wp_register_ability( $name, [ ..., 'safety' =&gt; [...] ] )<\/code> and helpers <code>abilityguard_rollback<\/code>, <code>abilityguard_snapshot_meta<\/code>, <code>abilityguard_snapshot_options<\/code>.<\/li>\n<li><strong>REST<\/strong>: <code>\/abilityguard\/v1\/log<\/code>, <code>\/log\/&lt;id&gt;<\/code>, <code>\/log\/export<\/code>, <code>\/rollback\/&lt;id&gt;<\/code>, <code>\/rollback\/bulk<\/code>, <code>\/approval<\/code>, <code>\/approval\/&lt;id&gt;\/approve<\/code>, <code>\/approval\/&lt;id&gt;\/reject<\/code>, <code>\/approval\/bulk<\/code>, <code>\/approval\/export<\/code>, <code>\/retention<\/code>, <code>\/retention\/prune<\/code>, <code>\/health<\/code>.<\/li>\n<li><strong>WP-CLI<\/strong>: <code>wp abilityguard log list\/show<\/code>, <code>wp abilityguard rollback &lt;id&gt;<\/code>, <code>wp abilityguard approval list\/approve\/reject &lt;id&gt;<\/code>, <code>wp abilityguard prune<\/code>.<\/li>\n<li><strong>wp-admin<\/strong>: Tools &gt; Tessera. Hybrid timeline + command-palette search, snapshot drawer, JSON-highlighted Input\/Result tabs, invocation chain navigation, and real rollback against the captured snapshot.<\/li>\n<\/ul>\n\n<h4>Example<\/h4>\n\n<pre><code>wp_register_ability( 'my-plugin\/update-product-price', array(\n    'label'               =&gt; 'Update product price',\n    'description'         =&gt; 'Updates the price on a WooCommerce product.',\n    'category'            =&gt; 'woocommerce',\n    'input_schema'        =&gt; array( \/* ... *\/ ),\n    'permission_callback' =&gt; fn() =&gt; current_user_can( 'manage_woocommerce' ),\n    'execute_callback'    =&gt; fn( $args ) =&gt; update_post_meta( $args['product_id'], '_price', $args['price'] ),\n    'safety' =&gt; array(\n        'destructive'       =&gt; true,\n        'requires_approval' =&gt; false,\n        'snapshot'          =&gt; fn( $input ) =&gt; array(\n            'post_meta' =&gt; array( $input['product_id'] =&gt; array( '_price', '_regular_price' ) ),\n            'options'   =&gt; array( 'woocommerce_last_price_change' ),\n        ),\n    ),\n) );\n<\/code><\/pre>\n\n<h4>Documentation<\/h4>\n\n<p>Full plugin-author documentation lives at the GitHub repo: https:\/\/github.com\/ibrahimhajjaj\/abilityguard<\/p>\n\n<h3>Source Code<\/h3>\n\n<p>The full source for Tessera, including the unminified React source for the admin app, lives on GitHub: https:\/\/github.com\/ibrahimhajjaj\/abilityguard<\/p>\n\n<ul>\n<li>The admin bundle <code>assets\/admin.js<\/code> is compiled from <code>assets\/admin.jsx<\/code> (React + JSX, no preprocessor magic beyond JSX).<\/li>\n<li>The bundler is <a href=\"https:\/\/esbuild.github.io\/\">esbuild<\/a>, configured in <code>scripts\/build.mjs<\/code>.<\/li>\n<li>To rebuild the admin bundle from a fresh checkout, run <code>npm install<\/code> once, then <code>npm run build<\/code> whenever <code>assets\/admin.jsx<\/code> changes. This regenerates <code>assets\/admin.js<\/code> in place.<\/li>\n<li>The release zip published to the WordPress.org directory is produced by <code>scripts\/build-release.sh<\/code>, which excludes development artifacts (tests, examples, build configs) but keeps everything required for the plugin to run.<\/li>\n<\/ul>\n\n<!--section=installation-->\n<ol>\n<li>Upload the <code>abilityguard-mcp<\/code> folder to <code>\/wp-content\/plugins\/<\/code>.<\/li>\n<li>Activate the plugin through the Plugins menu in WordPress (or network-activate on multisite).<\/li>\n<li>Visit Tools &gt; Tessera to view the audit log.<\/li>\n<li>In your own plugin, register abilities via <code>wp_register_ability()<\/code> with a <code>safety<\/code> config.<\/li>\n<\/ol>\n\n<p>Requires WordPress 6.9 or later (for the Abilities API) and PHP 8.1 or later.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"does%20this%20work%20without%20other%20plugins%3F\"><h3>Does this work without other plugins?<\/h3><\/dt>\n<dd><p>It will activate without registered abilities, but it only does work when other plugins register abilities with a <code>safety<\/code> config via <code>wp_register_ability()<\/code>.<\/p><\/dd>\n<dt id=\"what%20state%20surfaces%20are%20supported%20for%20snapshots%3F\"><h3>What state surfaces are supported for snapshots?<\/h3><\/dt>\n<dd><p>post_meta, options, taxonomy term assignments, user roles + caps, and files (with five tiered strategies from mtime to full content rollback).<\/p><\/dd>\n<dt id=\"does%20it%20support%20multisite%3F\"><h3>Does it support multisite?<\/h3><\/dt>\n<dd><p>Yes. Each subsite gets its own set of <code>wp_&lt;N&gt;_abilityguard_*<\/code> tables. New subsites are auto-installed via <code>wp_initialize_site<\/code>; deleted subsites have their tables dropped via <code>wpmu_drop_tables<\/code>.<\/p><\/dd>\n<dt id=\"how%20does%20it%20handle%20concurrent%20invocations%3F\"><h3>How does it handle concurrent invocations?<\/h3><\/dt>\n<dd><p>Per-surface MySQL advisory locks (GET_LOCK) serialise capture + execute so simultaneous invocations do not capture each other's mid-states.<\/p><\/dd>\n<dt id=\"are%20secrets%20encrypted%20in%20the%20log%3F\"><h3>Are secrets encrypted in the log?<\/h3><\/dt>\n<dd><p>Yes. Redaction uses AES-256-GCM envelopes so rollback can still restore the original value when the encryption key is intact.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.3.5<\/h4>\n\n<ul>\n<li>Release zip no longer ships <code>composer\/installers<\/code> and its unused installer adapters. Cuts the published zip from 909K to 824K and 206 files to 98.<\/li>\n<\/ul>\n\n<h4>1.3.4<\/h4>\n\n<ul>\n<li>Release zip now ships <code>vendor\/autoload.php<\/code> so the plugin actually boots on a fresh install. (1.3.3 zip was missing the autoloader and fatal'd on activation.)<\/li>\n<\/ul>\n\n<h4>1.3.3<\/h4>\n\n<ul>\n<li>Display name changed to \"Tessera for the Abilities API\" to clearly distinguish this plugin from any future official safety library. Slug, text domain, and internal namespace are unchanged.<\/li>\n<li><code>error_log()<\/code> calls in the rate-limiter and concurrency lock are now gated behind <code>WP_DEBUG<\/code>, so production hosts no longer accumulate noise from fail-open paths.<\/li>\n<li><code>readme.txt<\/code> gains a Source Code section documenting the GitHub repository, the esbuild-based build pipeline, and the <code>npm run build<\/code> command used to regenerate <code>assets\/admin.js<\/code>.<\/li>\n<\/ul>\n\n<h4>1.3.2<\/h4>\n\n<ul>\n<li>Slug renamed to <code>abilityguard-mcp<\/code> for the WordPress.org directory.<\/li>\n<li>Snapshot file blobs now stored under <code>wp-uploads\/abilityguard-mcp\/<\/code> instead of <code>wp-content\/abilityguard-staging\/<\/code>.<\/li>\n<li>Admin page CSS folded into the existing enqueued bundle; no more inline <code>&lt;script&gt;<\/code>\/<code>&lt;style&gt;<\/code> echoes.<\/li>\n<li><code>$_SERVER['REMOTE_ADDR']<\/code> is unslashed and sanitized before being hashed for IP-keyed rate-limit principals.<\/li>\n<\/ul>\n\n<h4>1.3.1<\/h4>\n\n<ul>\n<li>Skipped (broken release-workflow build).<\/li>\n<\/ul>\n\n<h4>1.3.0<\/h4>\n\n<ul>\n<li>Sliding-window-counter rate limiter with multi-policy support (burst + sustained), pluggable storage (Redis \/ object cache \/ transient), and IETF draft RateLimit headers.<\/li>\n<li>Dry-run mode: per-call <code>safety.dry_run<\/code> previews a destructive ability, persists the diff, auto-rolls-back, and surfaces details via <code>\/dry-run\/&lt;id&gt;<\/code> REST endpoint and <code>abilityguard_get_dry_run_result()<\/code> helper. Result returns untouched so it validates against <code>output_schema<\/code>.<\/li>\n<li>Approval queue gains per-stage role routing (<code>approval_roles<\/code>) and separation-of-duties enforcement across the chain.<\/li>\n<li>Per-status retention via <code>abilityguard_retention_days_by_status<\/code>.<\/li>\n<li><code>\/stats<\/code> REST endpoint and admin dashboard widget (counts, p50\/p95, top abilities).<\/li>\n<li>Wrapper split into observability listeners on <code>wp_before_execute_ability<\/code> \/ <code>wp_after_execute_ability<\/code> plus an enforcement seam (<code>abilityguard_pre_execute_decision<\/code> filter) for plugin extensions.<\/li>\n<li>Reads <code>meta.annotations.destructive<\/code> directly from core (WP 6.9 surface), no parallel safety metadata.<\/li>\n<li>Requires WP 6.9; pre-6.9 fallback path removed.<\/li>\n<\/ul>\n\n<h4>1.2.0<\/h4>\n\n<ul>\n<li>Parallel multi-stage approval chains with optional per-stage user pinning.<\/li>\n<li><code>\/health<\/code> REST endpoint and a pending-approvals badge in the admin bar.<\/li>\n<li>WP-CLI: <code>log show --diff<\/code>, <code>approval show<\/code>, <code>prune --all-sites<\/code>.<\/li>\n<li>JSONL export option for audit log.<\/li>\n<li>Real byte-level file rollback via <code>safety.snapshot.files.strategy = 'full_content'<\/code> (AES-256-GCM, content-addressed sidecar staging dir, atomic writes, 256 KB per-file cap).<\/li>\n<li>Full multisite support with auto-install on subsite creation and auto-drop on subsite deletion.<\/li>\n<li>Sequential and parallel multi-stage approval chains.<\/li>\n<\/ul>\n\n<h4>1.1.0<\/h4>\n\n<ul>\n<li>Multi-stage approval queues.<\/li>\n<li>Invocation correlation via <code>parent_invocation_id<\/code> and an admin-side invocation chain navigator.<\/li>\n<li><code>log_meta<\/code> table for extensible per-row metadata.<\/li>\n<\/ul>\n\n<h4>1.0.0<\/h4>\n\n<ul>\n<li>Initial public release.<\/li>\n<li>Snapshot, audit, rollback, and approval middleware for the WordPress Abilities API.<\/li>\n<li>Five collectors: post_meta, options, taxonomy, user_role, files.<\/li>\n<li>REST + WP-CLI + wp-admin surfaces.<\/li>\n<li>Encrypted redaction, payload caps, retention pruning.<\/li>\n<\/ul>","raw_excerpt":"Snapshot, audit, and rollback layer for plugins that register abilities via the WordPress Abilities API.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/306124","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=306124"}],"author":[{"embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/ibrahimhajjaj"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=306124"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=306124"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=306124"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=306124"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=306124"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=306124"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}