Snapshot + audit + rollback + approval middleware for the WordPress Abilities API.
Status: v1.2.0. Real byte-level file rollback via safety.snapshot.files.strategy = 'full_content', full multisite support, sequential + parallel multi-stage approval chains (with optional per-stage user pinning), and extensibility for custom collectors.
A developer library for plugin authors who register abilities via wp_register_ability() 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.
Declare what state your ability touches; AbilityGuard handles the safety wrapper.
Heads up: the Abilities API requires the ability's
categoryto already be registered onwp_abilities_api_categories_init, otherwise the registration silently no-ops. See docs/safety-config.md > Prerequisite for the one-liner.
wp_register_ability( 'my-plugin/update-product-price', array(
'label' => 'Update product price',
'description' => 'Updates the price on a WooCommerce product.',
'category' => 'woocommerce',
'input_schema' => array( /* ... */ ),
'permission_callback' => fn() => current_user_can( 'manage_woocommerce' ),
'execute_callback' => fn( $args ) => update_post_meta( $args['product_id'], '_price', $args['price'] ),
// AbilityGuard extension (stripped before core validation):
'safety' => array(
'destructive' => true,
'requires_approval' => false, // optional; true blocks until human approves
'snapshot' => fn( $input ) => array(
'post_meta' => array( $input['product_id'] => array( '_price', '_regular_price' ) ),
'options' => array( 'woocommerce_last_price_change' ),
// Other supported surfaces: 'taxonomy', 'user_role', 'files'
),
),
) );- Pre + post snapshots. Every safety-enabled invocation captures declared state before the callback and (on success) after - so the audit log can show a real diff, not just hashes.
- Audit log. One row per invocation with ability name, caller (
rest|mcp|cli|internal|...),caller_id(e.g. MCP server name when invoked through mcp-adapter), user, args, result, status, duration, pre/post hashes, andparent_invocation_idfor nested calls. - One-click rollback. Restore captured state from
post_meta,options, taxonomy term assignments, user roles + caps. File contents are not rewritten - instead,FilesCollectordoes tiered drift detection (mtime/mtime_size/critical_hash/full_hash), firesabilityguard_files_changed_since_snapshotandabilityguard_files_deleted_since_snapshot, and pins the changed/deleted paths onto the log row asfiles_changed_on_rollback/files_deleted_on_rollbackmeta. - Drift check on rollback. Live state is hashed and compared to the snapshot's post-state before restoring; if they differ the rollback returns
abilityguard_rollback_driftunless you passforce=trueor setsafety.skip_drift_check = trueon the registration. - Concurrency lock. Capture + execute is serialised per surface set via a MySQL advisory lock so two simultaneous invocations don't capture each other's mid-states.
- Encrypted redaction.
safety.redact/safety.scruband a globalabilityguard_redact_keysfilter scrub secrets out ofargs_json/result_json/ snapshot surfaces. v0.4+ stores redacted values as AES-256-GCM envelopes so rollback can still restore them when the key is intact. - Payload caps.
args_jsonandresult_jsonare capped (defaults 64 KB / 128 KB, filterable per-ability) with an explicit truncation marker so a runaway ability can't blow up the audit table. - Approval queue. When
safety.requires_approval => true, the wrapper blocks execution, logs the row aspending, persists the input, and returnsWP_Error('abilityguard_pending_approval', 202). A human approves or rejects via wp-admin,wp abilityguard approval, or REST. - Invocation correlation. Nested ability calls record a
parent_invocation_id, so the admin UI shows a click-through "Invocation chain" linking parents and children. - Retention. Daily WP cron prunes old log rows (defaults: 30 days normal, 180 days destructive) and orphaned snapshots. Both windows are filterable.
- MCP client attribution. When an ability is invoked via the WordPress MCP adapter, the audit row records which MCP server made the call.
- PHP API -
wp_register_ability( $name, [ ..., 'safety' => [...] ] )and helpersabilityguard_rollback,abilityguard_snapshot_meta,abilityguard_snapshot_options. - REST -
GET /abilityguard/v1/log,GET /log/<id>(returns log row + decoded snapshot + parent + children + log_meta),GET /log/export(CSV/JSON with the same filter args),POST /rollback/<id>,POST /rollback/bulk,GET /approval,POST /approval/<id>/approve,POST /approval/<id>/reject,POST /approval/bulk,GET /approval/export,GET /retention,POST /retention/prune. - WP-CLI -
wp abilityguard log list/show,wp abilityguard rollback <id>,wp abilityguard approval list/approve/reject <id>,wp abilityguard prune. - wp-admin - Tools → AbilityGuard. Hybrid timeline + command-palette search + per-day pagination, snapshot drawer, JSON-highlighted Input/Result tabs, "Invocation chain" navigation between parent and child invocations, "Rollback signals" card surfacing changed/deleted file paths, and real rollback against the captured snapshot.
Plugin-author guides live in docs/:
- docs/safety-config.md - Adding the safety config to your ability: full schema reference, snapshot resolver forms, all five surfaces, redaction, payload caps, and common mistakes.
- docs/approval-workflow.md - Approval queue: when to use
requires_approval, the approve/reject lifecycle, CLI commands, and integration recipes. - docs/custom-collectors.md - Writing your own collector: the
CollectorInterfacecontract, a worked example, current extensibility limits, and testing patterns. - docs/api-stability.md - Public API surface (every supported PHP function, action, filter, REST route, CLI command, and capability) plus the SemVer policy.
- docs/notifications.md - Wiring approval requests to Slack, email, Discord, Microsoft Teams, or generic webhooks. Drop-in
add_actionrecipes. - docs/multisite.md - Running AbilityGuard on a multisite network: network vs per-site activation, capabilities, cron pattern, file-blob staging, multinetwork handling, uninstall semantics.
examples/abilityguard-woocommerce-pack/ and examples/abilityguard-fluent-forms-pack/ - minimal third-party plugins that depend on AbilityGuard and demonstrate the safety config pattern. Each ships with its own integration test.
Requires PHP 8.1+, Composer, Node 20+, Docker.
composer install
npm install && npm run build # bundles assets/admin.jsx → assets/admin.js
# Pure unit tests (Hash + Json - fast, no DB).
composer test
# Lint + static analysis.
composer lint
composer stan
# Real integration tests against WordPress + MySQL via wp-env.
composer env:start
composer test:integration
composer env:stopwp-env is pinned to dev: 18888 / tests: 18889 so this project doesn't collide with other wp-env projects you might run on 8888.
The plugin's canonical slug is abilityguard (lowercase) - that's what the release zip ships, what wp.org expects, and what dependent plugins should declare in Requires Plugins: abilityguard.
Your local clone folder may be named AbilityGuard/ (capital A) or abilityguard/; .wp-env.json mounts the working tree under wp-content/plugins/abilityguard regardless via mappings, so dependents that declare Requires Plugins: abilityguard activate cleanly in dev. The plugin is auto-activated on wp-env start via lifecycleScripts.afterStart.
The integration suite is the primary correctness source - 140 tests covering installer schema, snapshot/audit/rollback round-trip, drift detection, encrypted redaction, the approval queue, all five collectors (including the FilesCollector tiered detection strategies), retention pruning, MCP identity, the post-state diff path, parent/child invocation correlation, and both example plugins.
If you use Claude Code worktrees, drop a local phpcs.xml (not committed) so PHPCS doesn't scan .claude/:
<?xml version="1.0"?>
<ruleset name="AbilityGuard (local)">
<rule ref="./phpcs.xml.dist"/>
<exclude-pattern>*/.claude/*</exclude-pattern>
</ruleset>- File-content rollback is opt-in via
strategy => 'full_content'. The four fingerprint strategies (mtime,mtime_size,critical_hash,full_hash) detect drift and fireabilityguard_files_changed_since_snapshot/abilityguard_files_deleted_since_snapshoton restore but don't rewrite bytes. The fifth strategyfull_contentactually captures and restores file contents - encrypted (AES-256-GCM), content-addressed in a sidecar staging dir, atomic temp-file+rename writes, octal-mode preserved, 256 KB per-file cap by default. Files over the cap fall back to fingerprint-only. - Approval queue supports multi-stage chains. Set
safety.requires_approval = ['stages' => [['cap' => 'X'], ['cap' => 'Y']]]to require sequential approvals from different capability pools.safety.requires_approval = truestays as the single-stage shorthand. Sequential semantics - any reject kills the chain. Parallel ("N of M must approve") is not modeled. - Multisite cron requires a real cronjob. WP-Cron is visit-driven; on low-traffic subsites it lags. Production deployments should call
wp abilityguard prune --all-sitesfrom a real system cron. See docs/multisite.md.
Four tables, all prefixed wp_abilityguard_:
log- one row per invocation.id, invocation_id, parent_invocation_id, ability_name, caller_type, caller_id, user_id, args_json, result_json, status, destructive, duration_ms, pre_hash, post_hash, snapshot_id, created_at.log_meta- extensible key/value attached to a log row. Currently used forskip_drift_check,files_changed_on_rollback,files_deleted_on_rollback. Read/write viaAbilityGuard\Audit\LogMeta.snapshots- gzipped pre-state and (when callback succeeds) post-state per invocation.approvals- pending approval requests whensafety.requires_approvalis set.
GPL-2.0-or-later.
