Los plugins sandboxed están aislados por defecto. Para hacer algo más allá de leer y escribir su propio KV y almacenamiento, un plugin debe declarar una capacidad en su manifiesto. El puente de la sandbox controla cada API proporcionada por el host basándose en estas declaraciones — un plugin que no declaró content:read no obtiene ctx.content, y uno que no declaró network:request no obtiene ctx.http.
Esta página cubre qué otorga cada capacidad, cómo la sandbox las aplica y qué no es aplicable.
Declarar capacidades
Las capacidades se encuentran en emdash-plugin.jsonc, junto con slug y el resto del contrato de confianza:
{
"slug": "plugin-hello",
// ...identidad + perfil...
"capabilities": ["content:read", "network:request"],
"allowedHosts": ["api.example.com"]
}
Declara solo lo que el plugin realmente necesita. Las declaraciones de capacidades también son lo que el Marketplace muestra a los operadores en el diálogo de consentimiento — capacidades adicionales son fricción en la instalación y una señal de seguridad en las auditorías.
Referencia de capacidades
| Capacidad | Otorga acceso a |
|---|---|
content:read | ctx.content.get(), ctx.content.list() |
content:write | ctx.content.create(), ctx.content.update(), ctx.content.delete() (implica content:read) |
taxonomies:read | ctx.taxonomies.getAll(), ctx.taxonomies.getTerms(), ctx.taxonomies.getEntryTerms() |
media:read | ctx.media.get(), ctx.media.list() |
media:write | ctx.media.getUploadUrl(), ctx.media.upload(), ctx.media.delete() (implica media:read) |
network:request | ctx.http.fetch() — restringido a allowedHosts |
network:request:unrestricted | ctx.http.fetch() sin restricción de host (solo para URLs configuradas por el usuario) |
users:read | ctx.users.get(), ctx.users.getByEmail(), ctx.users.list() |
email:send | ctx.email.send() (requiere un plugin proveedor de email configurado) |
hooks.email-transport:register | Permite registrar el hook exclusivo email:deliver (proveedor de transporte) |
hooks.email-events:register | Permite registrar hooks email:beforeSend / email:afterSend |
hooks.page-fragments:register | Permite registrar el hook page:fragments (solo plugins nativos) |
Algunas cosas a tener en cuenta:
- Implicaciones.
content:writeimplica automáticamentecontent:read;media:writeimplicamedia:read;network:request:unrestrictedimplicanetwork:request. No necesitas listar ambos. - Las taxonomías son una superficie separada de solo lectura.
taxonomies:readotorga acceso a las definiciones de taxonomías, sus términos y los términos asignados a una entrada a través dectx.taxonomies. Es independiente decontent:read— declara ambos si el plugin lee contenido y su clasificación. No hay acceso de escritura a taxonomías desde plugins. network:request:unrestrictedexiste para URLs configuradas por el usuario. Un plugin de webhook donde el operador ingresa la URL de destino necesita alcanzar hosts que no están en el manifiesto. Los plugins que siempre llaman a APIs conocidas deberían usarnetwork:request+allowedHosts.email:sendestá controlado por configuración, no solo por la capacidad. Un plugin puede declararemail:send, peroctx.emailsolo se completa si otro plugin ha registrado un transporteemail:deliver.
Listas de hosts permitidos para red
Los plugins con network:request solo pueden hacer fetch a hosts listados en allowedHosts. Se admiten comodines para subdominios:
"capabilities": ["network:request"],
"allowedHosts": [
"api.example.com", // host exacto
"*.cdn.example.com" // cualquier subdominio de cdn.example.com
]
El puente verifica el host de la URL de la solicitud contra la lista de permitidos antes de reenviar la solicitud. Una solicitud a un host no declarado lanza una excepción dentro del plugin sin salir nunca de la sandbox.
network:request:unrestricted omite completamente la verificación de la lista de permitidos. Está destinado a plugins donde el operador configura la URL de destino en tiempo de ejecución (emisores de webhook, reenviadores HTTP genéricos). Evítalo para plugins donde el destino es parte del diseño del plugin — declara network:request con hosts explícitos en su lugar para que el diálogo de consentimiento diga a los operadores exactamente a dónde llamará el plugin.
Lo que la sandbox aplica
Cuando un runner de sandbox está activo, el runtime aplica:
-
Control por capacidades. La fábrica de PluginContext solo completa
ctx.content,ctx.taxonomies,ctx.media,ctx.http,ctx.users,ctx.emailsi la capacidad correspondiente está declarada. Llamar a un método en una capacidad no declarada no es posible — no hay objeto allí. -
Alcance de almacenamiento y KV. Cada operación de almacenamiento y KV está limitada al slug del plugin. Un plugin no puede leer el KV o las colecciones de almacenamiento de otro, y solo puede acceder a colecciones de almacenamiento que declaró en el manifiesto.
-
Aislamiento de red.
fetch()directo y otras primitivas de red son bloqueadas por el runner. El único camino hacia la red esctx.http.fetch(), que pasa por la validación de host del puente. -
Sin bindings del host. Los plugins sandboxed no ven variables de entorno, sistema de archivos ni bindings de plataforma — incluso si tu worker host los tiene. El runtime del plugin es un aislado limpio con solo el puente y las capacidades declaradas.
-
Límites de recursos. El runner puede aplicar límites de CPU, sub-solicitudes, tiempo de reloj y memoria por invocación. Los límites exactos dependen de qué runner estés usando; el runner de Cloudflare usa los límites del Worker Loader de la plataforma (50ms CPU por invocación, 10 sub-solicitudes, 30 segundos de tiempo de reloj, ~128MB de memoria). El runner workerd de Node.js (
@emdash-cms/sandbox-workerd) aplica tiempo de reloj víaPromise.race; los límites de CPU y memoria son características de la plataforma Cloudflare y no se aplican por workerd independiente. Los hooks que exceden los límites del runner se cancelan; el timeout de hook de EmDash (timeouten la configuración del hook) aplica un límite superior más estricto además.
Lo que la sandbox no aplica
Algunas cosas que el sistema de capacidades no cubre ni puede cubrir:
- Comportamiento dentro de una capacidad otorgada. Un plugin con
content:writepuede editar cualquier contenido, no solo el suyo. Las capacidades son de grano grueso — dicen “este plugin puede escribir contenido”, no “este plugin solo puede escribir el contenido que creó”. La revisión en tiempo de auditoría es la única verificación de lo que un plugin realmente hace dentro de su concesión. - Confianza del operador en Node.js. Si el runner de sandbox configurado informa que no está disponible (sin Cloudflare Worker Loader, sin runner del lado de Node instalado, etc.), los plugins
sandboxed: []se omiten en el arranque. Puedes moverlos aplugins: []para ejecutarlos en proceso — pero entonces no hay aislado V8, no hay límites de recursos, y el plugin puede llamar afetch()directamente o leer variables de entorno. Trata eso como confianza a nivel nativo. - Canales laterales. Temporización, salida de logs y datos almacenados son visibles para cualquier persona con acceso razonable al entorno del host. No uses la sandbox como frontera de confidencialidad contra el operador que la ejecuta.
Consentimiento de capacidades
Cuando un operador instala un plugin sandboxed desde el Marketplace, EmDash muestra un diálogo de consentimiento con las capacidades declaradas. Las actualizaciones que agregan capacidades — por ejemplo, un plugin que antes solo leía contenido y ahora quiere hacer solicitudes de red — se muestran como un diff de capacidades y requieren aprobación nueva antes de que la nueva versión tome efecto.
Por eso es importante declarar capacidades adicionales incluso si “podrías necesitarlas más tarde”. Aparecen como fricción en cada instalación y actualización, y las auditorías de seguridad marcan plugins que piden más de lo que obviamente necesitan. Lista exactamente lo que el plugin usa y agrega nuevas capacidades en una versión real cuando el plugin realmente comience a usarlas.
Validación en tiempo de build
emdash-plugin bundle y emdash-plugin publish ejecutan verificaciones adicionales:
- Cada capacidad declarada debe estar en el conjunto reconocido (los errores tipográficos hacen fallar el build).
network:requestrequiere unallowedHostsno vacío;network:request:unrestrictedrequiere que esté vacío. Ver la referencia del manifiesto.- El
backend.jsempaquetado no puede importar built-ins de Node.js (fs,path,child_process, etc.) — los runtimes de sandbox no los proporcionan.
Ver Empaquetar y publicar para la lista completa de verificaciones.