Blocker for MCP launch
Extra-Chill/extrachill-mcp exposes the ability surface to external AI clients (ChatGPT, Claude, Gemini), scoped to the connected user's permissions. The intended product behaviour is that a user connects their own mailbox and asks their own AI client about their own email.
Today the email abilities cannot express that, and the gap is an authorization hole rather than a missing feature.
Current behaviour
FetchEmailAbility::checkPermission():
public function checkPermission(): bool {
return PermissionHelper::can( 'use_tools' ) || PermissionHelper::can_manage();
}
A flat capability check. No owner check, and the credential has no owner to check against — auth_ref defaults to email_imap:default, a single global handle.
So any user holding use_tools (the team tier on this install: several people) can fetch-email, email-reply, send-email, email-delete, email-move, and email-batch-unsubscribe against whatever mailbox is configured. Not because they were granted that mailbox — because the ability never models mailbox ownership.
send-email / email-reply additionally means sending as that identity.
Not currently exploitable: no mailbox is configured on this install and the PHP IMAP extension is absent, so execution errors early. This is about the shape being wrong before it is populated, not a live incident.
The primitive already exists and is already used elsewhere
public static function owns_resource( int $resource_user_id, string $action = 'manage_flows' ): bool {
if ( 0 === $resource_user_id ) { return true; } // single-agent mode
if ( self::has_privileged_resource_access( $action ) ) { return true; }
return self::acting_user_id() === $resource_user_id;
}
Real consumers today: ExecutionScope::owns_agent_resource(), Job/JobHelpers.php:73, Api/RestAccessGuard.php:107. Jobs and agent-scoped resources are correctly owner-checked.
The email abilities simply never adopted it. scope_user_id() has zero call sites anywhere in the plugin.
Work
- Give email auth refs an owner.
email_imap:default becomes a per-user record carrying user_id. This is the substantive change; the rest follows from it. Decide explicitly what happens to an existing unowned/global ref — owns_resource() already treats user_id === 0 as shared, which may be the right migration path for a genuinely shared operational mailbox, but that should be a decision rather than an accident.
- Gate the email abilities on ownership, resolving the ref's owner and calling
owns_resource() — matching what JobHelpers and RestAccessGuard already do. Small once step 1 lands.
- Provide a connect flow so a user can attach their own mailbox.
Wider than email
Any ability acting on a shared credential has this shape: gated on a capability rather than on holding the credential. data-machine-socials publishing is the obvious sibling — though a brand's Instagram is genuinely shared in a way an inbox is not, so the answer there may legitimately differ.
The model that gets this right already exists on the platform: venue grants in extrachill-events resolve booking_id to a venue and check that venue's grant per call. You get the venues you were granted, not all venues.
Worth deciding as a rule rather than case by case: abilities that act on a credential should be authorized by ownership of that credential, not by a generic tool-use capability.
Related
Extra-Chill/extrachill-mcp#7 — network-wide MCP endpoint (merged)
Extra-Chill/extrachill-network#208 — audit of the writes any account can reach
Extra-Chill/extrachill-network#207 — unfilterable WP-CLI permission bypasses
Blocker for MCP launch
Extra-Chill/extrachill-mcpexposes the ability surface to external AI clients (ChatGPT, Claude, Gemini), scoped to the connected user's permissions. The intended product behaviour is that a user connects their own mailbox and asks their own AI client about their own email.Today the email abilities cannot express that, and the gap is an authorization hole rather than a missing feature.
Current behaviour
FetchEmailAbility::checkPermission():A flat capability check. No owner check, and the credential has no owner to check against —
auth_refdefaults toemail_imap:default, a single global handle.So any user holding
use_tools(the team tier on this install: several people) canfetch-email,email-reply,send-email,email-delete,email-move, andemail-batch-unsubscribeagainst whatever mailbox is configured. Not because they were granted that mailbox — because the ability never models mailbox ownership.send-email/email-replyadditionally means sending as that identity.Not currently exploitable: no mailbox is configured on this install and the PHP IMAP extension is absent, so execution errors early. This is about the shape being wrong before it is populated, not a live incident.
The primitive already exists and is already used elsewhere
Real consumers today:
ExecutionScope::owns_agent_resource(),Job/JobHelpers.php:73,Api/RestAccessGuard.php:107. Jobs and agent-scoped resources are correctly owner-checked.The email abilities simply never adopted it.
scope_user_id()has zero call sites anywhere in the plugin.Work
email_imap:defaultbecomes a per-user record carryinguser_id. This is the substantive change; the rest follows from it. Decide explicitly what happens to an existing unowned/global ref —owns_resource()already treatsuser_id === 0as shared, which may be the right migration path for a genuinely shared operational mailbox, but that should be a decision rather than an accident.owns_resource()— matching whatJobHelpersandRestAccessGuardalready do. Small once step 1 lands.Wider than email
Any ability acting on a shared credential has this shape: gated on a capability rather than on holding the credential.
data-machine-socialspublishing is the obvious sibling — though a brand's Instagram is genuinely shared in a way an inbox is not, so the answer there may legitimately differ.The model that gets this right already exists on the platform: venue grants in
extrachill-eventsresolvebooking_idto a venue and check that venue's grant per call. You get the venues you were granted, not all venues.Worth deciding as a rule rather than case by case: abilities that act on a credential should be authorized by ownership of that credential, not by a generic tool-use capability.
Related
Extra-Chill/extrachill-mcp#7— network-wide MCP endpoint (merged)Extra-Chill/extrachill-network#208— audit of the writes any account can reachExtra-Chill/extrachill-network#207— unfilterable WP-CLI permission bypasses