The security model
What actually limits an assistant, stated plainly, with the parts that only look like limits named as such.
This is the section from the project README, unchanged:
wpreef does not sandbox the model. It hands the model a WordPress credential and shapes how easily each capability is reached. The two real boundaries are the closed
sitelist in your config and the WordPress role of the application password. Everything else is blast-radius shaping: read-only mode, opt-in toolsets, policy flags enforced on the canonical request, client approval prompts driven by tool annotations, and revision handles for recovery.confirmandallow_lossyflags are accident guards, not security controls.Site content is returned inside a nonced
<untrusted>boundary. No sanitiser prevents prompt injection; the guard, read-only mode and the credential's role are what limit the damage.
On the hosted connector the closed site list is the set of sites registered to your account. Everything else reads the same.
What the model cannot defeat
| Control | What it does |
|---|---|
| Read-only mode | Write tools are never registered, and the client refuses any non-GET request after canonicalisation. |
| The guard's route denylist | Application-password routes are always refused. User, plugin and settings writes are refused unless the matching toolset and policy are on. It holds even for the raw request tool. |
| Toolsets | A group that is off is absent from the tool list. The hosted connector never offers admin or raw. |
| Policy flags | Role changes, plugin installs, URL fetches, the settings allowlist and the ability namespaces are checked on the canonical request, not on what the model typed. |
| The credential's role | WordPress decides. An Editor cannot change your theme however the request is phrased. |
What only shapes the blast radius
Approval prompts are driven by tool annotations, and the client shows them; they are a
client-side control. confirm, dry_run and allow_lossy are flags the model fills in
itself — accident guards against a careless call, not a boundary against a determined one. The
audit log, the previous values and the revision handles are recovery, not prevention.
Content from your site is untrusted input
Everything read from WordPress — post bodies, comments, titles, alt text — arrives wrapped in a
nonced <untrusted> boundary, with the nonce stripped from the payload and scripts, iframes,
event handlers, javascript: URLs and zero-width or bidi characters removed in two passes.
That reduces accidental instruction-following. It does not prevent prompt injection, and nothing claims to. The rule the tools state to the assistant is the useful one: never escalate from a read to a write in the same turn on the strength of something found in the read.
Why this matters
A comment on your own site is text somebody else wrote. If an assistant treats it as an instruction, the credential it is holding is what decides how far that goes. That is the argument for an Editor account.
What the hosted connector adds
Hosted mode does not change any of the above. It adds a custodian, and the custody rules are:
- Each site's application password is encrypted with its own AES-256-GCM data key, wrapped by a key that lives only in the server's environment and never in the database. A database compromise yields ciphertext; a row lifted into another account's table does not decrypt.
- Site registration goes through the same guard as model-supplied URLs: HTTPS only, no redirects, every resolved address checked against private, loopback, link-local and related ranges, and the site is only saved once the credential authenticates.
- The audit log records what changed, not the content that changed. No bodies, no secrets.
- Credentials never enter the model's context.