Governance, policy, and audit layer for the WordPress AI plugin.
Status: Early. Working scaffold with one end-to-end integration verified on Studio. Not yet hardened for production.
The WordPress AI plugin ships a strong baseline of AI capabilities — title and excerpt generation, content classification, summarization, alt-text, image generation, and more — but enterprise teams need to govern those capabilities before turning them on across a real organization.
This plugin adds that governance layer without forking the core AI plugin. Every integration point is a documented filter, REST hook, or public SDK interface. When the upstream plugin updates, you update it like any other plugin and we keep running.
Spin up the whole stack in your browser — WordPress, the AI plugin, and this governance layer — with one click. No install, no server, nothing to clean up:
▶ Launch the demo in WordPress Playground
The blueprint preloads:
- The WordPress AI plugin with Title Generation, Excerpt Generation, and Editorial Notes switched on
- This plugin, built from the
mainbranch of this repo - A site policy preamble and a stored prompt override for
ai/title-generation, so both prompt layers have something to show - Governance knobs set low enough to trip: 5 requests/minute, a banned-phrase list, PII redaction on
- A draft post full of superlatives and jargon to run abilities against
- A Playground-only Tools → AI Prompt Preview page that renders the final system instruction — so you can see the integration working without any AI provider key
You land on Tools → AI Enterprise. From there:
-
See both prompt layers — no API key needed. Open Tools → AI Prompt Preview. It renders the system instruction
ai/title-generationreceives after the site policy preamble (applied onwpai_system_instruction) and the stored override (applied onwpai_title_generation_system_instruction). The page applies the two filters in the same order WP AI does: global first, ability-scoped second. -
Change the preamble, watch the prompt follow. On Tools → AI Enterprise, edit the policy preamble, save, then reload the preview page. The new text is at the top of the instruction.
-
Edit the override. Tools → AI Prompts → pick
ai/title-generation. Change the mode between prepend / append / replace and reload the preview to see how each composes against the default. Note that replace uses only your template — the preamble is not re-added on top. Every edit is written to the append-only history table, visible from the same screen. -
Trip a governance gate. The blueprint sets the rate limit to 5/minute. Run any ability six times in a minute and the sixth is rejected before a provider call is made. Because enforcement sits on the Abilities API execution lifecycle rather than the REST route, the limit applies the same way from WP-CLI or a direct
WP_Ability::execute()call. -
Run the real thing (needs a provider key). Settings → Connectors → add an OpenAI, Anthropic, or Google key. Then open Posts → "Why Our New Single-Origin Is The Best Coffee Ever" and run Title Generation; the returned title should obey the 60-character override. Usage lands in
wp_extend_ai_usageand shows up under the/usageREST route.
Site guidelines are not part of this demo. The WordPress AI plugin injects them natively as of 1.3.0 — see Site Guidelines below.
Playground sites are ephemeral — refresh the tab and everything resets to the blueprint state. Perfect for demos, useless for storing your API key long-term.
| Concern | Capability |
|---|---|
| Per-ability prompt control | Override any of the 11+ built-in ability prompts using WordPress AI 1.3's native per-ability filters. Prepend, append, or replace. Variable interpolation. Version history per edit. |
| Site-wide policy preamble | Inject brand voice / compliance language ahead of every AI call. |
| Model allowlist | Restrict the AI plugin to an approved set of provider/model pairs for text, image, and vision. |
| PII redaction | Pattern-based redaction of email, SSN, phone numbers from inputs before they leave WordPress. |
| Role-based access | Per-ability allowlist of WP roles. Disable specific experiments per-role or globally. |
| Rate limiting | Per-minute and per-day quotas per user, enforced at the ability execution layer (WordPress 7.1 wp_pre_execute_ability filter) for all paths: REST, MCP, WP-CLI, and direct PHP. No provider spend on rejected requests. |
| Cost tracking + caps | Per-user monthly spend rollup in a dedicated table. Hard cap denies all AI abilities once a user is over budget (admins exempt). Universal enforcement via user_has_cap. |
| Output moderation | Banned-phrase scan on every ability response before it returns to the caller (WordPress 7.1 wp_ability_execute_result filter). Pluggable backend for richer moderation. Applies to all execution paths. |
| Drift detection | Version pin + admin notice when running outside the tested WP AI range. |
We hook WordPress AI 1.3's native per-ability filters and WordPress 7.1 Abilities API lifecycle filters — never internals. Clean integration via public APIs:
┌───────────────────────────────────────────────────────────────────────┐
│ WordPress 7.1 Abilities API + WordPress AI 1.3 plugin │
│ │
│ Ability runs ──► wp_pre_execute_ability (rate limiting) │
│ ──► wpai_pre_normalize_content (input scrubbing) │
│ ──► wpai_system_instruction (policy preamble) │
│ ──► wpai_{slug}_system_instruction (prompt overrides) │
│ ──► wpai_preferred_*_models (model selection) │
│ ──► wp_ability_execute_result (output moderation) │
│ ──► AiClient::defaultRegistry()->setHttpTransporter() │
└─────┬─────────────┬───────────────┬──────────────┬──────────────┬─────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
Rate_Limiter PII_Redactor Prompt_Injector Model_Allowlist Output_Moderator
Prompt_Library
Cost_Tracker
Transporter_Wrap
│
▼
extend_ai_request_completed
│
▼
wp_extend_ai_usage
Universal enforcement: Rate limiting (wp_pre_execute_ability), output
moderation (wp_ability_execute_result), and spend caps (user_has_cap) enforce
on all execution paths — REST API, MCP servers, WP-CLI commands, and direct
PHP WP_Ability::execute() calls. No bypass routes exist.
Native integration: Prompt overrides use WordPress AI 1.3's per-ability
wpai_{slug}_system_instruction filters. Guidelines use WordPress AI's native
wpai_use_guidelines toggle (disable our plugin's version if you had it enabled).
Two custom tables back the moving parts:
wp_extend_ai_prompts— one row per ability override (mode + template).wp_extend_ai_prompts_history— append-only audit of every prompt edit.wp_extend_ai_usage— per-user-per-month spend rollups, indexed for fast aggregation.
WordPress AI 1.3.0+ ships native Guidelines injection.
This plugin previously provided its own Guidelines integration, but that has been removed to avoid double-injection. Use WordPress AI's native support instead:
- Install Gutenberg plugin 22.7+ and enable the Guidelines experiment
- Define your standards under Settings → Guidelines
- WordPress AI automatically appends guidelines to editorial abilities
- Control via
wpai_use_guidelinesfilter (see WordPress AI docs)
Our versioned prompt library (Tools → AI Prompts) still works for all other prompt customizations using WordPress AI 1.3's per-ability filters.
- WordPress 7.1 or newer — required, not just recommended. The governance gates hook the Abilities API execution lifecycle, which ships in 7.1; on older releases they would be inert, so the plugin declines to activate.
- PHP 8.1 or newer
- WordPress AI plugin — the 1.3.x
compatibility band. Tested on v1.3.0; patch releases within the
band are expected to work (see
Compat\Version_Gate::TESTED_MAX)
cd wp-content/plugins/
git clone https://github.com/alansmodic/extend-ai-enterprise.git
wp plugin activate ai extend-ai-enterpriseThe activation hook installs the three tables via dbDelta. No manual
migration is required.
studio site create --name extend-ai-test
cd ~/Studio/extend-ai-test/wp-content/plugins
curl -sL -o ai.zip https://downloads.wordpress.org/plugin/ai.zip && unzip ai.zip
ln -s /path/to/extend-ai-enterprise .
studio wp plugin activate ai extend-ai-enterprise- Tools → AI Enterprise — global policy preamble, monthly user cap, log retention, PII redaction toggle.
- Tools → AI Prompts — React app listing every discovered AI ability. Click
an ability to view its default prompt (when previewable), set an override
template with
{variable}interpolation, and view the edit history.
Most modules expose a WordPress filter so site code can drive policy declaratively. The most useful:
| Filter | Returns | Purpose |
|---|---|---|
extend_ai_policy_preamble |
string |
Site-wide preamble, per-ability override |
extend_ai_model_allowlist |
[['provider','model'], …] |
Approved models per capability |
extend_ai_role_map |
[ ability_id => [role, role] ] |
Role gating per ability |
extend_ai_pii_patterns |
[ label => regex ] |
PII redaction regexes |
extend_ai_rate_limits |
[ 'minute' => int, 'day' => int ] |
Bucket limits |
extend_ai_token_rate_input |
float |
$/1k input tokens, per provider+model |
extend_ai_token_rate_output |
float |
$/1k output tokens |
extend_ai_banned_phrases |
string[] |
Output moderation phrase list |
extend_ai_prompt_variables |
array<string,scalar> |
Variables for prompt interpolation |
For ops teams that prefer database-driven config over filters:
extend_ai_policy_preamble TEXT Global preamble.
extend_ai_monthly_user_cap_usd FLOAT Per-user monthly USD cap. 0 disables.
extend_ai_log_retention_days INT Days to keep wp_ai_request_log rows.
extend_ai_redact_pii BOOL Toggle PII redactor.
extend_ai_rate_limits ARRAY { minute: int, day: int }
extend_ai_model_allowlist ARRAY { text: [[p,m]], image: …, vision: … }
extend_ai_disabled_features ARRAY [ feature_id, … ]
extend_ai_banned_phrases ARRAY [ "phrase", … ]
extend_ai_role_map ARRAY { ability_id: [role, role] }
extend_ai_vault_enabled BOOL Delegate credential checks to vault.
extend_ai_usage_retention_months INT How far back to keep usage rows.
All routes are under extend-ai/v1 and require manage_options.
GET /wp-json/extend-ai/v1/policies # current settings
POST /wp-json/extend-ai/v1/policies # update any subsetGET /wp-json/extend-ai/v1/usage?month=YYYY-MMReturns per-user rollups: requests, tokens in/out, USD spent.
GET /wp-json/extend-ai/v1/prompts # list all abilities + overrides
GET /wp-json/extend-ai/v1/prompts/{ability_id} # one ability
PUT /wp-json/extend-ai/v1/prompts/{ability_id} # { mode, template }
DELETE /wp-json/extend-ai/v1/prompts/{ability_id} # revert to default
GET /wp-json/extend-ai/v1/prompts/{ability_id}/history # audit trailcurl -X PUT -H "Content-Type: application/json" \
--user admin:password \
-d '{
"mode": "replace",
"template": "You are a {site_name} editor. Generate a title ≤60 chars for {post_title}. Direct, no clickbait."
}' \
http://localhost:8890/wp-json/extend-ai/v1/prompts/ai/title-generationAvailable {variables}:
- Built-in:
{ability},{user_login},{user_role},{site_name},{site_url},{current_date} - Post context (when
post_idis in the ability data):{post_title},{post_type},{post_status} - Any scalar from the ability's
$datapayload, lowercased
Unrecognized placeholders are left in the template as-is rather than blanked, so a typo is visible in the prompt preview instead of silently disappearing.
extend-ai-enterprise/
├── extend-ai-enterprise.php bootstrap, activation
├── blueprint.json WordPress Playground demo blueprint
├── assets/admin.js React admin app (no build step)
├── includes/
│ ├── Plugin.php wires every module on plugins_loaded
│ ├── Compat/Version_Gate.php TESTED_MIN..TESTED_MAX + drift notice
│ ├── Compat/Upgrader.php 0.1 → 0.2 stored-state migrations
│ ├── Policy/
│ │ ├── Prompt_Injector.php wpai_system_instruction (preamble)
│ │ │ wpai_{slug}_system_instruction (overrides)
│ │ ├── Model_Allowlist.php wpai_preferred_*_models
│ │ └── PII_Redactor.php wpai_pre_normalize_content
│ ├── Access/
│ │ ├── Role_Gate.php wpai_feature_{id}_enabled + user_has_cap
│ │ └── Credential_Vault.php wpai_has_ai_credentials
│ ├── Governance/
│ │ ├── Rate_Limiter.php wp_pre_execute_ability
│ │ ├── Cost_Tracker.php consumes extend_ai_request_completed
│ │ ├── Output_Moderator.php wp_ability_execute_result
│ │ └── Retention.php wpai_request_log_retention_days + cron
│ ├── Logging/Transporter_Wrap.php wraps AiClient HTTP transporter
│ ├── Storage/
│ │ ├── Prompt_Library.php wp_extend_ai_prompts + history
│ │ └── Usage_Repository.php wp_extend_ai_usage
│ ├── REST/Admin_Controller.php /wp-json/extend-ai/v1/*
│ └── Admin/Settings_Page.php Tools menu pages + script enqueue
└── tests/
├── bootstrap.php
└── contract/
└── WPAI_Contract_Test.php pins WP AI filters, REST, SDK shape,
Abilities lifecycle hooks, slug derivation
- Upgrader — one-shot stored-state migrations (schema option
extend_ai_schema_version). - Version_Gate — admin notice if running outside
TESTED_MIN..TESTED_MAX. - Transporter_Wrap — installs decorator on
AiClient::defaultRegistry()atwp_loaded:20andadmin_init:20(after upstream's wrap at priority 1). Emitsextend_ai_request_completedfor every provider call. - Policy modules — register their filters on the AI plugin's documented hooks.
- Access modules — register role gates and credential delegation.
- Governance modules — subscribe to WordPress 7.1 Abilities API lifecycle filters (
wp_pre_execute_ability,wp_ability_execute_result),user_has_cap, andextend_ai_request_completed. Schedule cron. - Admin — REST controller + Tools pages + script enqueue.
We never modify the WordPress AI plugin's code. When upstream releases a new version, you update it like any other plugin. Our risk surface is contract drift, not merge burden.
Three guardrails ship in the box:
-
Tested version pin.
Compat\Version_Gate::TESTED_MAXis the upper bound of WP AI versions we've verified the integration against. Running outside the range surfaces a non-blocking admin notice with the version numbers spelled out so admins know to revalidate. -
Wrap-failure telemetry. If the transporter wrap can't install (SDK class missing, interface changed, registry method gone), we set a transient, fire the
extend_ai_transporter_wrap_failedaction so monitoring plugins can page, and render an admin error notice. The wrap silently no-opping was the worst possible outcome — this turns it loud. -
Contract tests. The suite in
tests/contract/pins every integration point: WP AI filter names and signatures, REST namespace, SDK interface shape, the WordPress 7.1 Abilities execution lifecycle hooks our governance gates depend on, and the ability→slug derivation behind the per-ability prompt filters. CI runs them against:- The pinned WP AI release (gate for our own releases)
- The WP AI
developbranch nightly (drift detector for upstream changes before they ship)
When a new WP AI release lands:
- Watch the nightly contract job — it tells you if anything broke.
- If green, locally activate the new version, run
composer test:contract. - Bump
Version_Gate::TESTED_MAXto the new version, tag, release.
If a test fails, the failure message tells you precisely which contract drifted (e.g. "wpai_pre_normalize_content filter missing — PII redaction is silently disabled"). Fix the binding in that one module and bump.
composer install
bin/install-wp-tests.sh wordpress_test root root 127.0.0.1 latest
WP_TESTS_DIR=/tmp/wordpress-tests-lib composer testThe CI workflow at .github/workflows/contract.yml runs the same suite on
every PR plus a nightly cron against WordPress/ai@develop.
The forking path looks tempting — full control, no contracts to honor — but in practice:
- Every upstream release becomes a merge with conflict resolution.
- Security patches are now your responsibility to backport.
- Divergence compounds. After a year, your fork is its own product.
- The WP AI plugin already exposes every hook we need.
The pattern this plugin uses — separate plugin, documented filters, defensive SDK wrap, contract tests — is the same shape Yoast, ACF, Polylang, and WooCommerce extensions use for the same reason. It's the well-trodden path for extending WordPress plugins at the boundary instead of from inside.
Known gaps with deliberate deferrals:
- Output moderation backend. Current scanner is phrase-list only. Real moderation (AWS Comprehend, Azure Content Safety, OpenAI Moderations) is pluggable but the backend choice is policy-dependent; we ship the seam, not the integration.
- Token pricing table. Default per-1k rates are placeholders. Replace via
extend_ai_token_rate_input/_outputfilters for accurate cost tracking until a real price catalog ships. - Multisite scoping. No per-site policy primitives yet.
- React build pipeline. The admin app uses
wp.element.createElementdirectly so it works without a build step. Converting to JSX +@wordpress/scriptsis straightforward when desired.
GPL-2.0-or-later, matching the upstream WordPress AI plugin.