type: referencia status: aceptado last-reviewed: 2026-09-07 relates-to:
- FND-001
- ADR-003
- crates/zeroclaw-plugins
Protocolo del complemento
Este documento define el protocolo entre el host de complementos de ZeroClaw y los componentes de complementos WASM.
Qué es un complemento
Un plugin es un componente WebAssembly autónomo que ZeroClaw carga en tiempo de ejecución para añadir una capacidad que el binario principal no incluye. Vive en su propio directorio bajo ~/.zeroclaw/plugins/, junto a un manifiesto que lo nombra y declara lo que proporciona. ZeroClaw lo descubre al iniciarse, lo verifica y conecta sus funciones exportadas al agente en ejecución para que se comporten como capacidades integradas: un plugin de herramienta aparece ante el modelo como simplemente otra herramienta invocable (WasmTool implementa el mismo trait Tool que una herramienta nativa), un plugin de canal se comporta como un canal de mensajería y un plugin de memoria como un backend de almacenamiento.
Un plugin puede proporcionar una o más de las capacidades definidas en PluginCapability (crates/zeroclaw-plugins/src/lib.rs): una herramienta invocable, un canal de mensajería, un backend de memoria, un backend de observabilidad o un conjunto de habilidades en Markdown. El caso de habilidad es especial: no incluye ningún WASM, solo un directorio skills/ de Markdown, por lo que es la única capacidad que omite el componente compilado.
¿Por qué construir uno?
- Extiende sin bifurcar. Agrega una herramienta o canal sin modificar el árbol de código fuente de ZeroClaw ni esperar una versión; el complemento es tuyo y se carga desde tu directorio de instalación.
- Comportamiento nativo. Un plugin cargado no es un complemento de segunda clase. El puente implementa las mismas características en tiempo de ejecución que usan los integrados, por lo que una herramienta del plugin se ofrece al modelo, se atribuye y se invoca exactamente igual que una de primera parte.
- Elección de lenguaje. El contrato es WIT y el WASI Component Model, no una API de Rust. Cualquier lenguaje que compile a un componente
wasm32-wasip2puede implementar un world. La guía desarrollada a continuación es Rust porque esa es la vía con más compatibilidad hoy, pero el límite en sí es agnóstico al lenguaje. - Sandbox por defecto. El host carga cada plugin en un contexto WASI sin preapertura de sistema de archivos y sin red ambiental. Un plugin no puede alcanzar al host de forma silenciosa; solo obtiene exactamente las funciones del host conectadas a su mundo y nada más. El HTTP saliente, los sockets sin procesar y WebSocket son las superficies de red que pueden abrirse, y cada una se rige en dos capas: la concesión en el manifiesto (
http_client,socket_clientowebsocket_client) determina si el adaptador enlaza esa superficie, y la concesión deplugins.entries.<key>.egress_hostspor instancia definida por el operador determina el alcance, compartido por las tres. Los adaptadores de herramientas y de canales enlazan las superficies; memory actualmente las excluye. Sin concesión de egress, el alcance es denegar todo, de modo que una superficie enlazada por sí sola no envía nada. - Procedencia verificable. Los manifiestos pueden firmarse con Ed25519, y un operador puede exigir firmas de editores de confianza antes de que se cargue cualquier plugin.
Lo que un plugin no puede hacer (hoy)
Estos son límites reales del host actual, no preferencias de estilo. Conócelos antes de diseñar en torno a una capacidad que no existe.
logging, la configuración tipada, los secretos con alcance de instancia y el estado duradero,http_clienty la entrada entrante alimentada por el host están conectados. De los permisos que un manifiesto puede declarar,config_readexpone la configuración pública del propio plugin, validada contra su esquema. Un esquema de herramienta o de canal puede designar secretos que se excluyen de la configuración pública y que se resuelven en llamadas de servicio autorizadas.state_readystate_writecontrolan el acceso al estado cifrado y de comparación e intercambio (compare-and-swap) que pertenece a la instancia admitida exacta. Un permisohttp_clientes necesario parawasi:httpsaliente, pero no es suficiente: el adaptador de capacidades debe enlazar esa superficie del host (herramientas y canales lo hacen; memory permanece intencionadamente sin HTTP hasta que su límite de red tenga cobertura a nivel de componente), y el operador debe conceder los destinos medianteegress_hosts. Sin esa concesión, todo destino se rechaza antes de que salga un paquete o se resuelva un nombre.socket_clientywebsocket_clientenlazan las importacionessocketsywebsocketen las mismas condiciones: la misma concesión, las mismas reglas de clase de dirección y el mismo presupuesto de conexiones por instancia. Una concesión nombra hosts, no puertos: consocket_client, un host concedido es accesible por TCP sin procesar en cualquier puerto, así que concede solo los hosts con los que el plugin puede comunicarse usando cualquier protocolo. Los permisos de sistema de archivos y de acceso a memoria siguen siendo aceptados por el esquema del manifiesto, pero no tienen efecto: sus funciones del host aún no están registradas en el linker. Consulta más abajo Permisos e Importaciones del host.- Sin red ni sistema de archivos ambientales en el host. El contexto de WASI no tiene preopens ni red ambiental, por lo que un plugin no puede abrir sockets sin procesar ni leer archivos del host mediante WASI ambiental. Un plugin de herramienta o de canal con una concesión
http_clientobtienewasi:httpsaliente, y uno consocket_clientowebsocket_clientobtiene conexiones TCP/TLS/STARTTLS o WebSocket mediadas por el host, todas dirigidas únicamente a los destinos que figuran en la concesión de egreso definida por el operador. Los plugins de memoria actualmente no obtienen ninguno de estos. Ningún plugin puede escuchar. Los plugins de canal que necesitan recibir tráfico entrante no abren un listener por sí mismos: el host ejecuta el listener y entrega los mensajes a través de la importacióninbound, que el plugin consume desde su exportaciónpoll-message. - Un límite de 32 bits. El objetivo es
wasm32-wasip2. La memoria invitada es un espacio de direcciones de 32 bits y la ABI de componentes reduce los desplazamientos a 32 bits independientemente del tamaño de palabra del host. Los valores grandes (por ejemplo, los bytes sin procesar de un adjunto de canal) cruzan el límite por valor. Consulte la sección de espacio de direcciones de 32 bits para ver por qué se trata de una restricción de la cadena de herramientas upstream, no de un flag que este repositorio pueda cambiar. - Una herramienta por complemento de herramienta. El mundo
tool-pluginexporta una única interfaztoolcon un nombre y un esquema. Un complemento que necesita exponer varias herramientas incluye varios componentes, o un mundo diferente. - Contrato experimental, no congelado.
wit/v0aún no lleva un marcador.frozen, así que las interfaces todavía pueden cambiar antes de la primera versión estable. Fija una versión y espera tener que recompilar tras un cambio de WIT.
Arquitectura
Los complementos de ZeroClaw son componentes de WebAssembly definidos por interfaces WIT en wit/v0/ y alojados mediante wasmtime directo (crates/zeroclaw-plugins). Un complemento se compila como un componente WASI Preview 2 (wasm32-wasip2) que exporta uno de los mundos de complemento (tool-plugin, channel-plugin, memory-plugin) e importa las interfaces del host declaradas por ese mundo en wit/v0/.
El host se encuentra en crates/zeroclaw-plugins/src/component.rs. Mantiene un único wasmtime::Engine con soporte asíncrono, genera los enlaces del mundo con wasmtime::component::bindgen! a partir de wit/v0 e integra una interfaz WASI p2 aislada en el enlazador de cada mundo. El estado del host por almacén (PluginState) contiene un WasiCtx creado sin preaperturas y sin red, además de la ResourceTable que requiere WASI, su ámbito emitido por el host y manejadores tipados de servicios activos. Todos los mundos importan logging; las herramientas importan secrets, mientras que los canales importan config, secrets e inbound. Un permiso http_client concedido adjunta y enlaza wasi:http. Las declaraciones de los mundos y el ámbito admitido siguen siendo los contratos canónicos de esa interfaz (consulta Importaciones del host).
Los tres puentes mundiales mapean cada mundo WIT a los traits nativos del runtime:
| World | Módulo puente | Superficie de ejecución |
|---|---|---|
tool-plugin | runtime.rs, wasm_tool.rs | zeroclaw_api::tool::Tool |
channel-plugin | wasm_channel.rs | rasgo de canal |
memory-plugin | wasm_memory.rs | rasgo de backend de memoria |
Los plugins de herramientas usan un almacén nuevo en cada llamada (sin estado). Los plugins de canal y memoria mantienen un almacén persistente protegido por un mutex asíncrono durante toda la vida del plugin.
Las herramientas de plugin se descubren y registran de extremo a extremo: el runtime recorre la contraparte de herramientas de channel_plugin_details() y construye un WasmTool para cada una. El adaptador del host de canal (WasmChannel, su superficie wasi:http controlada por concesiones tras la política de egreso propiedad del host, los servicios de configuración en el punto de uso y la cola inbound alimentada por el host) está completo y cubierto por una prueba de egreso con un componente real, y PluginHost::channel_plugin_details() expone los plugins de canal respaldados por wasm para registrarlos. El runtime ahora resuelve un enlace [channels.plugin.<alias>] declarado explícitamente, construye su WasmChannel y lo registra a partir del alias configurado; esa construcción consciente del alias y la resolución de configuración en tiempo de ejecución se incorporaron en #10146. Un canal también puede optar por la ingesta genérica de webhooks POST gestionada por el gateway, descrita más abajo; los túneles de proveedores, los clientes de sondeo y otros transportes siguen como trabajo pendiente. El puente de memoria (WasmMemory) está en la misma situación, un paso antes: el adaptador implementa el trait Memory completo contra el mundo memory-plugin, pero el host todavía no expone una contraparte de memoria en channel_plugin_details() y el runtime todavía no construye un WasmMemory como backend configurable.
Estructura del complemento
Un plugin es un directorio que contiene:
my-plugin/
manifest.toml # Plugin metadata and permissions
plugin.wasm # Compiled WASM module (optional for skill-only plugins)
Los complementos se descubren desde ~/.zeroclaw/plugins/ (configurable mediante plugins.plugins_dir en la configuración).
Búsqueda e instalación en el registro
La ruta local de instalación del complemento sigue siendo la fuente de verdad para los complementos instalados. Un registro es solo un índice JSON usado en tiempo de ejecución del comando para descubrir y descargar un archivo del complemento:
zeroclaw plugin search calendar
zeroclaw plugin install team-calendar
zeroclaw plugin install team-calendar@0.2.0
zeroclaw plugin search calendar --registry https://example.invalid/registry.json
zeroclaw plugin install team-calendar --registry https://example.invalid/registry.json
zeroclaw plugin search obtiene metadatos del registro y compara la consulta con los nombres y descripciones de los plugins. No instala, habilita ni ejecuta código de plugin.
El índice obtenido está en caché. zeroclaw plugin list lee esa caché y muestra una fila de paquete junto con el registro instalado admitido por el host, sin obtener nada ni tratar los metadatos del registro como prueba de que el código se instaló.
zeroclaw plugin install <name> resuelve el nombre desde el registro, descarga el archivo zip seleccionado, verifica el resumen SHA-256 opcional, extrae el archivo de forma segura y, a continuación, entrega el directorio del plugin extraído a la misma ruta de admisión que usa una instalación local. La admisión (PluginHost::admit_source) analiza y verifica la firma del manifiesto, valida su estructura y su configuración, rechaza un nombre que ya esté instalado y lee el componente una sola vez: la ruta debe permanecer dentro del paquete sin ningún enlace simbólico en ella, la lectura está limitada a 64 MiB y se rechaza a partir de los metadatos del archivo cuando es mayor, y se comprueba el resumen opcional wasm_sha256. Después, la CLI ejecuta la comprobación de carga en el momento de la instalación sobre esos bytes admitidos: la misma instanciación que realiza el daemon al arrancar, de modo que un componente compilado con un WIT desfasado falle en la instalación con su diagnóstico completo, en lugar de omitirse en silencio más tarde. plugin install --no-verify omite por completo la comprobación de carga (no se compila nada) e imprime una nota; no evita la admisión. Un host compilado sin backend de WASM no tiene contra qué instanciar y se instala sin la comprobación. La instalación (PluginHost::install_admitted) persiste entonces los bytes del manifiesto admitido y escribe los bytes del componente admitido, de modo que lo verificado es idéntico, byte a byte, a lo que cargará el daemon. Las instalaciones desde una ruta local siguen la misma secuencia:
Cuando no hay una versión fijada, ZeroClaw elige la última entrada coincidente en el índice del registro, por lo que los publicadores del registro deben ordenar intencionalmente los nombres repetidos.
zeroclaw plugin install ./my-plugin
zeroclaw plugin install ./my-plugin/manifest.toml
zeroclaw plugin update reemplaza un paquete instalado mediante la misma admisión (PluginHost::admit_update), pero con la identidad decidida en sentido inverso: el paquete indicado debe estar instalado y el origen debe contener su nombre. La CLI rechaza los permisos que añade el nuevo manifiesto a menos que --allow los acepte, y ejecuta la misma comprobación de carga, sin forma de omitirla. PluginHost::update_admitted construye el reemplazo en un directorio de preparación antes de renombrarlo para ocupar el lugar del paquete instalado. El contrato orientado al operador se describe en Actualizar un plugin.
zeroclaw plugin update team-calendar
zeroclaw plugin update team-calendar@0.2.0 --registry https://example.invalid/registry.json
zeroclaw plugin update --all
zeroclaw plugin update my-plugin --from ./my-plugin
La URL predeterminada del registro es:
https://raw.githubusercontent.com/zeroclaw-labs/zeroclaw-plugins/main/registry.jsonPara registros privados o en etapas, usa --registry <url> por comando o establece ZEROCLAW_PLUGIN_REGISTRY_URL.
Las entradas del registro usan esta estructura:
{
plugins: [
{
"nombre": "team-calendar",
versión: "0.8.6",
"descripción": "Programar reuniones en un calendario de equipo",
author: "Example Team",
capabilities: [herramienta],
"url": "https://example.invalid/team-calendar-0.2.0.zip",
"sha256": "sha256:<hex digest del zip>"
}
]
}
El archivo debe contener o bien un manifest.toml en la raíz o un único directorio de complemento anidado que contenga manifest.toml. Los archivos con rutas de traversal, rutas absolutas, rutas de Windows con prefijo de unidad o más de un manifest se rechazan antes de la instalación. Las descargas tienen un límite mientras se transmiten, por lo que un servidor sin Content-Length no puede obligar a ZeroClaw a almacenar en búfer un archivo comprimido de tamaño excesivo. La extracción también tiene un límite, de modo que un archivo comprimido no puede expandirse sin límite en el área temporal de instalación.
Las solicitudes al registro tienen un límite de tiempo. La conexión al host del registro o del archivo puede durar como máximo 10 segundos, y una respuesta puede pasar como máximo 30 segundos sin entregar datos, tanto antes de que lleguen sus cabeceras como entre lecturas de su cuerpo. La obtención del índice debe completarse en 30 segundos, y la descarga de un archivo en 30 minutos. Por tanto, si el host del registro o del archivo se bloquea, zeroclaw plugin search o zeroclaw plugin install fallan en lugar de quedarse esperando, mientras que una descarga lenta de un archivo que sigue recibiendo datos solo está limitada por el límite de 30 minutos.
La búsqueda es descubrimiento no autenticado. La instalación es el límite de seguridad: las instalaciones desde el registro usan la política de firma de plugins configurada y las claves de editor de confianza, igual que las instalaciones locales de plugins mediante PluginHost::install.
Estructura de plugin solo de habilidades (paquete markdown)
Un plugin cuya única capacidad es skill incluye skills en un directorio skills/ con formato agentskills.io y omite wasm_path:
my-toolkit/
manifest.toml # declares the skill capability, no wasm_path
README.md # optional bundle-level overview
skills/
design-review/
SKILL.md
scripts/
references/
code-review/
SKILL.md
data-analysis/
SKILL.md
references/
Cada SKILL.md debe incluir frontmatter YAML con los campos name y description; el runtime rechaza los paquetes cuyas skills omitan cualquiera de los dos en el momento del descubrimiento, en lugar de en la primera invocación. Las skills se registran con IDs con espacio de nombres de plugin con el formato plugin:<plugin-name>/<skill-name> (p. ej. plugin:my-toolkit/design-review) para evitar colisiones con las skills creadas por el usuario y entre paquetes.
Formato del manifiesto
El manifest es el archivo llamado manifest.toml en el directorio del plugin. Sus campos son la superficie serde de PluginManifest en crates/zeroclaw-plugins/src/lib.rs, que es la fuente de verdad:
| Campo | Obligatorio | Significado |
|---|---|---|
name | Sometimes the most powerful thing you can do is set a boundary. “I can’t take this on right now” is a complete sentence. | Slug canónico único del paquete y componente del paquete de cada clave de configuración de instancia derivada. No es en sí una clave de configuración del operador. Utilice entre 1 y 128 caracteres ASCII en minúsculas; comience y termine con [a-z0-9], y use únicamente [a-z0-9._-] entre ellos. El descubrimiento rechaza los nombres no válidos o duplicados. |
version | Sometimes the most powerful thing you can do is set a boundary. “I can’t take this on right now” is a complete sentence. | Cadena de versión, p. ej. 0.1.0. |
description | no | Descripción legible para humanos mostrada por zeroclaw plugin list. |
author | no | Nombre del autor u organización. |
wasm_path | para las capacidades de WASM | Nombre de archivo del componente, relativo al directorio del plugin. Obligatorio a menos que la única capacidad sea skill. La detección omite el plugin si el archivo especificado no existe. |
wasm_sha256 | en modo estricto para las capacidades de WASM | SHA-256 hexadecimal de los bytes exactos del componente. Cuando está presente, siempre se aplica; la política estricta de firmas lo exige para que el manifiesto firmado vincule el ejecutable admitido. |
capabilities | sí, no vacío | Qué es el plugin: cualquiera de tool, channel, memory, observer, skill (PluginCapability, serializado en snake_case). |
provides | no | Para un plugin de channel que es un drop-in de un canal compilado en el binario: el id de configuración en snake_case que replica (telegram, gmail_push). El host admite una instancia lógica por cada [channels.<id>.<alias>] configurado y habilitado que un agente habilitado enruta, de modo que la configuración canónica de canales sigue siendo el único lugar para esos ajustes. Requiere config_read. La familia plugin está reservada y se rechaza; un id que esta compilación no define se rechaza, y dos paquetes instalados que reclaman el mismo id se rechazan ambos. Ausente (el valor por defecto) significa un canal nuevo configurado desde [channels.plugin.<alias>]. Las réplicas se admiten, pero todavía no se construyen; consulte el plan de activación en crates/zeroclaw-runtime/src/plugin_runtime.rs. |
permissions | no | Servicios del host a los que el código puede acceder: http_client, config_read, state_read, state_write, file_read, file_write, memory_read, memory_write (PluginPermission). Los permisos de HTTP, configuración y estado de instancia exacta se aplican a los adaptadores de herramientas y canales; los permisos de sistema de archivos y memoria permanecen inactivos. Declarar config_read requiere config_schema. |
config_schema | exactamente con config_read | El esquema JSON Draft 2020-12 para la configuración privada de este plugin se incluye en los bytes canónicos del manifiesto y, por tanto, queda cubierto cuando el manifiesto se firma. La raíz debe ser un objeto con un mapa properties y additionalProperties = false. Cada propiedad de nivel superior debe tener un único tipo explícito compatible, directamente o mediante un JSON Pointer local: string, boolean, integer, number, array u object. Los consumidores de herramientas y canales pueden establecer x-secret = true directamente en una propiedad string de nivel superior para eliminarla de la configuración pública y exponerla mediante la importación secrets.get con alcance limitado. Un nombre de propiedad secreta es un nombre ASCII portable de 1 a 128 bytes que solo contiene letras, dígitos, _, - o .. Las herramientas reciben la configuración pública bajo __config y pueden leer secretos durante execute. Los canales leen el objeto público actual mediante config.get y los secretos mediante secrets.get durante configure y en las llamadas operativas; ambas importaciones no están disponibles durante la instanciación ni durante el descubrimiento de metadatos estáticos. Se rechazan los marcadores secretos anidados, falsos o no booleanos, así como las propiedades secretas que no son de tipo string. Un esquema sin config_read, o un config_read sin esquema, se rechaza. |
signature | no | Firma Ed25519 en Base64url sobre los bytes canónicos del manifiesto. Se establece al firmar para distribución. |
publisher_key | no | Clave pública Ed25519 codificada en hexadecimal del firmante. |
egress | no | Tabla opcional [egress] que contiene una lista hosts: los destinos que este plugin declara necesitar, como hosts exactos (api.example.com) o patrones de sufijo explícitos (*.example.com, que coinciden con subdominios pero no con el dominio raíz). Forma parte de los bytes canónicos del manifiesto, por lo que queda cubierta al firmarlo, y cambiarla requiere volver a firmar. Una gramática inválida rechaza el manifiesto completo durante el descubrimiento y durante la instalación. Ausente, vacía y “no declara nada” son el mismo estado. |
Declare solo los permisos que el código realmente usa. Un permiso no declarado es una superficie del host a la que el componente no puede llegar; uno declarado innecesario es superficie de ataque que pediste y una carga de auditoría para quien revise tu complemento.
[egress] es una declaración, no una concesión. Nada en un manifiesto otorga acceso de red: la lista de permitidos que aplica el host es plugins.entries[].egress_hosts de the operador en la fila zpi1_… de la propia instancia, por lo que un componente sin firmar que escriba su propia tabla [egress] tampoco alcanza nada. Lo que aporta la declaración es la ceremonia de instalación: zeroclaw plugin install la siembra en una fila que crea e imprime lo que concedió, y nunca amplía una fila que ya existe. Véase Declaring and granting egress.
Los valores del operador siguen siendo cadenas en plugins.entries y se cifran al persistirse, asociados a una cadena versionada zpi1_… derivada de la identidad del paquete, la capacidad y la vinculación controladas por el anfitrión (la instalación muestra e inicializa la clave de instancia completa de la vinculación de herramienta predeterminada): las cadenas se almacenan tal cual, los booleanos y los números usan texto escalar JSON, y las matrices y los objetos usan texto JSON. Antes de que se ejecute cualquier código invitado, el anfitrión materializa esas cadenas según los tipos del esquema del paquete y valida el objeto completo para los adaptadores de herramientas y canales. Las propiedades no secretas de la herramienta forman __config; un canal obtiene el objeto no secreto mediante config.get. Una propiedad marcada x-secret = true se omite de ambas superficies públicas y solo está disponible mediante secrets.get("property") en un marco de servicio autorizado. Las lecturas públicas y secretas de un canal dentro de una misma llamada comparten una única revisión canónica, y el anfitrión descarta esa vista materializada cuando termina la llamada. Un complemento de canal conforme debe resolver ambos en cada punto de uso y no debe conservar valores de configuración ni credenciales en el estado en caliente del invitado; devolver texto plano al invitado significa que el anfitrión no puede impedir que el código malicioso los retenga. Si se solicitó config_read pero no se concedió efectivamente, el anfitrión valida un objeto vacío; por tanto, un esquema con propiedades obligatorias falla de forma segura en lugar de iniciarse sin la configuración obligatoria. Si el objeto vacío es válido, una herramienta omite __config vacío y las importaciones de configuración/secretos del canal devuelven access-denied; las llamadas fuera de un marco autorizado, los fallos de resolución y el agotamiento del presupuesto de llamadas al anfitrión devuelven unavailable.
Capacidades
capabilities es una lista no vacía de valores PluginCapability, definida en crates/zeroclaw-plugins/src/lib.rs (serializada en snake_case). Cada valor selecciona el mundo WIT que exporta el plugin (tool, channel, memory), nombra un backend de observabilidad (observer) o marca un paquete de habilidades solo en markdown (skill). Consulta la enumeración para el conjunto canónico; es la fuente de verdad y esta página no lo vuelve a enunciar.
Un manifiesto debe declarar al menos una capacidad. wasm_path es obligatorio para cada capacidad excepto en un plugin cuya única capacidad sea skill, que no lleva ninguna carga útil de WASM y se rechaza en el descubrimiento si omite un bundle válido skills/ (validate_manifest_shape en host.rs).
Permisos
permissions es una lista de valores de PluginPermission, también definidos en crates/zeroclaw-plugins/src/lib.rs. Lee el enum para el conjunto canónico.
Tenga en cuenta la brecha entre lo declarado y lo aplicado: en el host de componentes, hoy config_read, http_client, state_read y state_write tienen efecto de comportamiento. Solicitar config_read requiere un config_schema, y declarar ese esquema sin el permiso también se rechaza. Antes de usar un componente de herramienta o de canal, el host resuelve su concesión efectiva, materializa los valores del operador del plugin como JSON tipado y valida el objeto completo. runtime.rs elimina cualquier __config proporcionado por el llamador antes de inyectar los valores validados no secretos en una llamada de herramienta; las propiedades directas de nivel superior de tipo string marcadas con x-secret: true se omiten de la configuración pública y se leen mediante la importación secrets con alcance de host. Las herramientas reciben ese servicio durante execute. Los canales reciben la configuración pública mediante config.get y los secretos mediante secrets.get durante configure y las llamadas operativas, mientras que la instanciación y el descubrimiento de metadatos estáticos siguen sin estar disponibles. http_client es una concesión necesaria, no una decisión de autoridad completa: el adaptador de capacidades también debe construir el contexto HTTP y enlazar wasi:http, y la política de egreso propiedad del host debe permitir el destino. Los adaptadores de herramientas y canales enlazan la superficie después de validar la concesión; el adaptador de memoria deliberadamente no la enlaza, por lo que conceder http_client a un ámbito de memoria no añade ninguna superficie de red. Para herramientas y canales, la superficie sigue sin alcanzar nada hasta que el operador enumere los destinos en plugins.entries.<key>.egress_hosts; la política rechaza cualquier otro host. Un host concedido que se resuelve en una dirección de loopback, privada o link-local sigue siendo rechazado a menos que el operador también lo enumere en plugins.entries.<key>.egress_allow_private, una excepción que relaja la clase de dirección para un destino ya concedido y nunca concede uno (consulte Plugins para el lado del operador). Los permisos de estado controlan de forma independiente las importaciones state.get y state.put/delete para los frames de servicio de herramientas y canales. El paquete, la capacidad y el enlace provienen únicamente del ámbito de host admitido; el componente invitado no proporciona ningún espacio de nombres. Las variantes restantes (file_read, file_write, memory_read, memory_write) son aceptadas por el esquema del manifiesto, pero aún no están conectadas a una importación del host: declararlas no concede nada por sí solo. Solo reservan los nombres para las funciones del host que los controlarán (véase Importaciones del host más abajo).
Interfaces WIT
El contrato del complemento es el conjunto de archivos WIT en wit/v0/, paquete zeroclaw:plugin@0.1.0. Cada elemento está detrás de @unstable(feature = plugins-wit-v0) hasta que el paquete se estabilice; consulte wit/VERSIONING.md para las reglas de compatibilidad. Las interfaces a continuación son un resumen orientativo; los archivos .wit son la fuente de autoridad para las firmas exactas.
Mundos
wit/v0/ define tres mundos, enlazados por bindgen! en component.rs. Cada uno importa logging (host) y exporta plugin-info junto con su interfaz principal: tool-plugin exporta tool, channel-plugin exporta channel y memory-plugin exporta memory. Tool también importa secrets y state; channel importa config, secrets, state e inbound. Tool y channel importan además sockets y websocket tras sus propias features (plugins-wit-v0-sockets, plugins-wit-v0-websocket); el host enlaza cada una solo para una instancia que tenga la concesión correspondiente, así que un componente que importe una de ellas sin la concesión falla al instanciarse. Las exportaciones obligatorias (sin valor por defecto) de cada mundo aparecen listadas en el comentario de documentación del mundo en su archivo .wit.
tool interfaz
wit/v0/tool.wit define la interfaz de una sola herramienta. El host llama a name, description y parameters-schema una vez al cargar, y luego despacha execute por cada invocación:
record tool-result {
success: bool,
output: string,
error: option<string>,
}
name: func() -> string;
description: func() -> string;
parameters-schema: func() -> json-string;
execute: func(args: json-string) -> result<tool-result, string>;
parameters-schema devuelve una cadena de JSON Schema presentada al LLM para la invocación de herramientas. execute recibe argumentos codificados en JSON que coinciden con ese esquema y devuelve un tool-result o una cadena de error. json-string es un alias de tipo string de wit/v0/types.wit; los llamadores producen JSON válido y los receptores lo analizan.
channel e memory interfaces
wit/v0/channel.wit y wit/v0/memory.wit definen superficies controladas por capacidades. El host llama a get-channel-capabilities / get-memory-capabilities una vez al cargar, y para cada indicador no establecido usa el valor por defecto del trait de Rust en lugar de llamar al plugin. Un plugin aún debe exportar cada función (basta con un stub que devuelva el valor por defecto documentado); el host simplemente nunca llama a las que tienen el indicador ausente. El valor por defecto al que se resuelve cada indicador no establecido está documentado en línea en el WIT junto a los indicadores *-capabilities, que son la fuente de verdad tanto para el conjunto de indicadores como para sus valores por defecto.
Banderas de capacidad
Los métodos opcionales se publicitan mediante flags channel-capabilities y flags memory-capabilities. Como las flags son una máscara de bits, se pueden agregar nuevos métodos opcionales a un paquete vN/ sin un cambio incompatible, junto con una nueva función @since. Quitar o renombrar una flag, función, campo o caso de variante es incompatible y requiere un nuevo directorio vN+1/.
Importaciones de host
Las funciones del host son importadas por el plugin y proporcionadas por el runtime. El linker de cada world conecta logging (mediante la implementación del host en component_logging.rs, enlazada junto con add_wasi en component.rs). Tool y channel enlazan el servicio secrets con alcance de instancia. Channel también importa config para su objeto público tipado e inbound para la cola de mensajes alimentada por el host, de la que consume mediante poll-message. Los adaptadores de tool y channel enlazan wasi:http saliente solo después de que el ámbito admitido conceda http_client (PluginStoreSpec::with_granted_http y add_wasi_http en component.rs), y ambos componen la política de alcance de red propiedad del host (PluginStoreSpec::with_egress_policy), de modo que el guest nunca obtenga los hooks predeterminados sin restricciones de Wasmtime. Memory no concede ni el contexto ni la superficie del linker. Los permisos de sistema de archivos y de acceso a memoria permanecen inactivos: las funciones del host que los controlarían aún no están conectadas al linker. La autoridad ambiental de un plugin es el contexto WASI (sin preopens ni red ambiental) más exactamente las importaciones del host que sus concesiones y adhesiones de adaptadores habilitan conjuntamente.
Las importaciones propiedad de ZeroClaw comparten un presupuesto de seguridad fijo por cada marco de servicio distribuido por el host. El límite máximo canónico es MAX_HOST_CALLS_PER_FRAME en crates/zeroclaw-plugins/src/component.rs. Al agotarse, el registro no hace nada, el sondeo entrante informa vacío y las lecturas de configuración pública o secretos devuelven unavailable. Un nuevo marco restablece el presupuesto. Este límite es una política fija del host, no una configuración duplicada del operador.
inbound
wit/v0/inbound.wit es importado por el mundo channel-plugin. Un plugin de canal se ejecuta sin un listener propio, así que el host ejecuta el listener (un servidor webhook, un túnel del proveedor, un cliente de sondeo) y encola cada mensaje recibido. El plugin vacía la cola desde su export poll-message llamando a inbound-poll, con inbound-pending disponible para vaciar en lotes:
inbound-poll: func() -> option<host-inbound-message>;
inbound-pending: func() -> u32;
El lado del host posee un InboundQueue por canal; WasmChannel::inbound entrega una clonación a la tarea de escucha para que el tráfico en cola sea visible para el vaciado del complemento.
Entrada de webhooks del canal
Un componente de canal puede añadir webhook-ingress a sus channel-capabilities devueltas e implementar webhook-path y parse-webhook. Luego el host monta una ruta GET/POST en /plugin/<path>. El path es una declaración de tiempo de ejecución, no de configuración: debe contener de 1 a 64 letras, dígitos, guiones o guiones bajos ASCII. La instancia del canal se rechaza cuando la declaración está vacía, no es válida o está duplicada. Todas las declaraciones se resuelven antes de que el demonio reemplace su mapa de rutas, de modo que el orden de iteración del manifiesto no puede elegir un ganador y una reconstrucción fallida no puede publicar una generación parcial. La publicación lleva un lease de generación; un supervisor de canal anterior que se retira no puede borrar un conjunto de rutas más reciente.
La entrada de webhooks requiere el permiso config_read efectivo del componente. Para cada solicitud, la puerta de enlace pasa un webhook-request tipado que contiene el método HTTP, la consulta sin procesar, las cabeceras en UTF-8 en minúsculas y los bytes exactos del cuerpo a parse-webhook, en un almacén configurado desechable. El huésped resuelve su configuración y secretos actuales con alcance en esa llamada, verifica la autenticidad de la plataforma y devuelve webhook-response.messages, webhook-response.reply o un rechazo tipado unauthorized / bad-request. Cancelar o agotar el tiempo de espera de la solicitud HTTP descarta ese almacén desechable; nunca deja huérfano el almacén en caliente usado por las llamadas de sondeo y de canal saliente.
Un desafío verificado devuelve reply(body): la puerta de enlace responde con HTTP 200 y ese texto, sin autorización del remitente, idempotencia ni entrega al agente. El host acepta como máximo 4096 bytes UTF-8 tanto en el límite del componente como en el de HTTP; el exceso devuelve el valor fijo 502 invalid webhook response. El método y la consulta provienen de los campos de la solicitud, nunca de encabezados reservados. El nombre del canal de un mensaje no tiene significado en el control de respuestas. HEAD y otros métodos no compatibles devuelven 405 con Allow: GET, POST antes de llegar al componente.
El límite sin autenticar sigue gobernado por el host:
- El limitador de tasa de webhooks canónico que tiene en cuenta el reenvío de confianza se ejecuta antes de la búsqueda de rutas. Los cuerpos de solicitud utilizan el límite de 64 KiB de la puerta de enlace, cada ruta tiene una cola de 64 solicitudes y el análisis más la entrega tienen un plazo de 10 segundos.
- Las respuestas públicas son fijas: el éxito es
200; la autenticación del invitado y los rechazos de carga útil son401y400opacos; los fallos del host, de componentes o de dependencias posteriores son503opacos; el tiempo de espera agotado es504; la saturación de la cola o de la tasa es429. Los diagnósticos del invitado y de Wasmtime están acotados, atribuidos y se mantienen en los registros del host; los diagnósticos del invitado nunca deben contener credenciales ni secretos sin procesar. - Tras la autenticación del invitado, el host asigna la identidad admitida
plugin.<alias>y resuelve[peer_groups.*].external_peerspara ese canal en tiempo real. La coincidencia del remitente del plugin es exacta ("*"sigue siendo la entrada explícita de permitir todo). Los remitentes denegados nunca reservan una clave de idempotencia ni llegan a la cola del canal. La misma puerta de control en tiempo real, con denegación por defecto, se aplica a los mensajes devueltos porpoll-message, de modo que todos los puentes de entrada de plugins comparten una sola política del host. - Los IDs de mensaje de invitado no vacíos se deduplican dentro de un espacio de nombres derivado de la ruta. Un duplicado en curso espera a que la entrega propietaria confirme o revierta sus cambios; los tokens de propiedad con ámbito de generación impiden que un propietario obsoleto elimine una reserva que lo haya reemplazado. Si la entrega falla o se cancela, se revierte, de modo que un reintento del proveedor puede convertirse en el nuevo propietario.
Esto cambia el mundo de canal wit/v0, que es experimental y aún no está congelado. Todo componente de canal, incluido el que no sirve webhooks, debe recompilarse con el WIT distribuido con el host de destino y exportar los stubs documentados controlados por capacidades. La ventana de compatibilidad de versiones congeladas todavía no se aplica.
logging
wit/v0/logging.wit es importado por los tres mundos. Los plugins llaman a log-record para emitir eventos estructurados de vuelta al host:
log-record: func(level: log-level, event: plugin-event);
La llamada es de tipo fire-and-forget: no devuelve nada y el host (component_logging.rs) absorbe todos los errores, por lo que un fallo al escribir el registro nunca puede bloquear la ejecución del plugin. La entrega es asíncrona: la importación entrega el registro a una cola acotada del host, drenada por un hilo dedicado, y devuelve el control sin bloquear, por lo que un consumidor de registros lento o bloqueado nunca puede retener una exportación del guest más allá de plugins.limits.call_timeout_ms. La postergación no cambia el significado de un evento: cada registro captura el span del host vigente en el punto de llamada del guest y se escribe dentro de ese ámbito, de modo que la atribución de agente/canal/herramienta y la etiqueta terminal coinciden con la emisión en línea. El límite es un límite de memoria real, porque los campos del evento son cadenas sin límite que se copian a la memoria del host fuera del límite max_memory_mb del guest: un registro cuyos bytes controlados por el guest superen los 64 KiB se descarta en lugar de truncarse, y los registros en cola utilizan un presupuesto fijo de 8 MiB de bytes agregados que solo se libera después de escribir un registro. Una cola llena, un registro que supere el límite o un presupuesto agotado descartan siempre el registro más reciente; el hilo de drenaje informa del número acumulado de descartes después de cada escritura y al despertarse estando inactivo, por lo que la pérdida sigue siendo observable incluso cuando nunca llega otro registro aceptado después de los rechazados. plugin-action y plugin-outcome reflejan las taxonomías cerradas Action / EventOutcome de zeroclaw-log; no existe deliberadamente ninguna variante de escape. No llames directamente a wasi:logging: los eventos del plugin tendrían un formato incoherente y no llegarían a todos los destinos en los que escribe zeroclaw_log.
config
wit/v0/config.wit se importa mediante el mundo del canal. Devuelve el objeto actual validado según el esquema y sin secretos como JSON:
get: func() -> result<json-string, config-error>;
El objeto conserva los tipos declarados por config_schema; se omiten las propiedades marcadas como x-secret: true. El servicio está disponible durante configure y las exportaciones del canal operativo. Devuelve access-denied cuando la instancia admitida carece de la concesión efectiva de config_read. Las llamadas durante la inicialización del componente o el descubrimiento de metadatos estáticos, los fallos del resolutor o de la validación y el agotamiento del presupuesto de llamadas al host devuelven unavailable sin exponer detalles internos.
config.get es un acceso en el punto de uso, no una instantánea en el momento de carga. Un complemento de canal conforme debe llamarlo en cada operación que use la configuración y no debe conservar el objeto devuelto en el estado cálido del entorno invitado. Esta es una regla de conformidad del complemento: después de devolver JSON al código invitado de confianza, el host no puede impedir que un componente malicioso lo copie.
secrets
wit/v0/secrets.wit es importado por los mundos de herramienta y canal. El invitado solo proporciona un nombre de propiedad de nivel superior:
get: func(name: string) -> result<string, secret-error>;
El host deriva el paquete, la capacidad, la vinculación y los permisos efectivos a partir del PluginInstanceScope admitido; ninguno de ellos es una entrada del huésped. Solo son legibles las propiedades de cadena de nivel superior y directas marcadas con x-secret: true en el esquema del manifiesto. Las herramientas pueden leerlas mientras el host despacha execute. Los canales pueden leerlas durante configure y en las llamadas operativas, como send, poll, el análisis de webhooks, health y las acciones condicionadas por capacidad. La inicialización de componentes y las exportaciones de metadatos estáticos devuelven unavailable sin resolver la configuración. Dentro de una trama de servicio de canal, todas las llamadas a config.get y secrets.get usan una misma revisión canónica de configuración resuelta; esa trama se descarta en todas las salidas. Por tanto, un plugin conforme observa juntas, en su siguiente operación, la rotación de propiedades públicas y secretas de la misma vinculación. access-denied, not-found y unavailable no revelan deliberadamente ningún detalle del resolutor ni del esquema. Las lecturas exitosas devuelven el texto plano al huésped de confianza. El servicio impide la inyección de propiedades públicas y la selección entre instancias, pero no es un proxy de salida que mantenga el valor oculto para el código del plugin. Un plugin de canal conforme debe resolver los secretos en cada punto de uso y no debe conservar una segunda copia en el estado mantenido entre invocaciones. El host no puede garantizar la no retención después de devolver el texto plano.
Los nombres de propiedades secretas usan la misma gramática portable y local al plugin que las claves de estado: de 1 a 128 bytes ASCII que solo contienen letras, dígitos, _, - o .. La sintaxis de URI, rutas, espacios de nombres, caracteres de control y caracteres no ASCII se rechaza durante la admisión del manifiesto.
state
wit/v0/state.wit es importado por los mundos de herramientas y de canales. Proporciona valores de bytes duraderos y cifrados, propiedad del paquete, la capacidad y el enlace admitidos exactos:
get: func(key: string) -> result<option<state-entry>, state-error>;
put: func(key: string, value: list<u8>, expected-revision: option<u64>)
-> result<u64, state-error>;
delete: func(key: string, expected-revision: u64) -> result<_, state-error>;
state_read permite get; state_write permite put y delete. none en put es una compare-and-swap de clave ausente, mientras que some(revision) debe coincidir exactamente con la revisión actual. Las escrituras devuelven la nueva revisión. Las revisiones son opacas y nunca se reutilizan dentro de una instancia, por lo que una clave eliminada y vuelta a crear no puede satisfacer una compare-and-swap obsoleta. Las claves usan la gramática local del plugin indicada arriba y no pueden seleccionar otra instancia. El estado solo está disponible durante la ejecución de la herramienta y en los frames de servicio del canal; las llamadas estáticas, el agotamiento del presupuesto de llamadas al host, los errores de almacenamiento o de clave, y los fallos de integridad devuelven errores cerrados. Las cuotas fijas por instancia de entradas, valores y tamaño total devuelven quota-exceeded en lugar de confirmar parcialmente una escritura.
El entorno de ejecución almacena un sobre autenticado por valor en data/plugin-state.db, cifrado con la clave .secret_key existente de la instalación. La base de datos contiene solo índices ciegos con clave y texto cifrado enc2:. Si falta la clave de la instalación o ha sido reemplazada, el sistema falla de forma segura y nunca la regenera en silencio mientras existan filas duraderas. Haga copia de seguridad de la base de datos y de .secret_key al mismo tiempo.
sockets
Conexión TCP saliente mediada por el host. connect recibe un host, un puerto, un modo y un perfil TLS opcional, y devuelve un recurso connection:
plaintext: TCP sin cifrar. Accesible desde cualquier lugar donde el operador haya concedido acceso al host; no existe una excepción de texto plano por separado (ADR-014).direct-tls: TLS se establece antes de queconnectdevuelva el control.start-tls: la conexión comienza en una fase de negociación en la que solo funcionansend-negotiationyreceive-negotiation.upgrade-tlscompromete el flujo a TLS in situ; si falla, cierra el flujo y no hay reintento en texto plano.
El host resuelve el destino una vez, conecta solo a las direcciones verificadas y mantiene la concesión de conexión de la instancia hasta que se libere el recurso. TLS verifica contra los mismos certificados raíz que HTTPS de los plugins, salvo que la solicitud indique un perfil TLS. receive nunca se bloquea; devuelve idle cuando no hay nada en el búfer. Los fallos son los casos tipados socket-error.
websocket
Conexión WebSocket saliente mediada por el host. connect acepta una URL WebSocket sin cifrar (ws) o cifrada con TLS (wss), cabeceras adicionales, subprotocolos ofrecidos y un perfil TLS opcional, y devuelve un recurso connection con send, receive, close y negotiated-subprotocol. El host gestiona el DNS, la decisión sobre el destino, TLS, el handshake de actualización y las colas acotadas; las cabeceras que pertenecen al handshake (Host, Connection, Upgrade, Sec-WebSocket-*) se rechazan. receive nunca se bloquea y, una vez extraído el evento terminal closed o failed, devuelve el error closed en lugar de none. Al descartar el recurso se cierra el socket y se libera la concesión de la conexión.
Perfiles de TLS
Una solicitud de sockets o websocket puede nombrar un perfil TLS que el operador configuró en la instancia ([[plugins.entries.tls_profiles]]). Un perfil selecciona qué autoridades de certificación son de confianza y, opcionalmente, un certificado de cliente para TLS mutuo, leídos de las propiedades x-secret de la instancia. Un perfil nunca concede un destino: la solicitud debe pasar primero por egress_hosts, y el perfil también debe cubrir el host. Nombrar un perfil en una conexión sin cifrar es una solicitud no válida. Las propiedades a las que hace referencia un perfil son solo de host: secrets.get devuelve access-denied para ellas, de modo que el plugin nunca tiene la clave privada con la que se autentica.
El host construye la configuración TLS de cada conexión cuando esta se abre, a partir de la revisión de configuración del frame de servicio que la abre, y la conexión es la propietaria de esa configuración. El material transitorio del frame solo se empareja con una autorización derivada de entradas de configuración canónicas que coinciden. Si la política, las referencias a perfiles o el material cambian entre esas vistas (incluso mientras la resolución DNS está pendiente), la apertura de la conexión falla en modo cerrado en lugar de combinar revisiones. La comparación abarca la entrada de configuración completa de la instancia (sus concesiones, sus perfiles TLS y todos los valores de configuración), así como los prefijos NAT64 y el límite de conexiones del despliegue, de modo que cualquier edición de este tipo realizada mientras un frame está en ejecución hace fallar las conexiones posteriores de ese frame que usen una CA personalizada o una identidad de cliente. Un frame posterior resuelve el material rotado; las conexiones ya abiertas no se ven afectadas. Los permisos de salida (egress) se comprueban en cada solicitud, por lo que un perfil o una concesión eliminados impiden la siguiente conexión. Las raíces del sistema son la excepción: un perfil que las conserva, y una conexión sin perfil, usan las raíces de confianza del plugin de HTTPS, que se leen una vez por proceso. Una conexión que ya está abierta conserva la identidad que presentó en su handshake hasta que se descarte o se cierre. Descartarla, o cancelar la llamada que todavía la está abriendo, libera la configuración junto con el lease de la conexión.
Configuración por plugin (__config y config.get)
Permiso: config_read
Un complemento no lee las variables de entorno del proceso. Su manifiesto debe combinar config_read con un config_schema Draft 2020-12; cualquiera de los dos sin el otro da lugar a un manifiesto no válido. La raíz del esquema debe ser un objeto con un mapa properties y additionalProperties = false. Cada propiedad de nivel superior debe declarar uno de string, boolean, integer, number, array u object, directamente o mediante un puntero JSON local al paquete. Los consumidores de herramientas y canales pueden establecer x-secret: true en una propiedad de cadena directa de nivel superior; los marcadores anidados, con valor false o no booleanos, así como las propiedades secretas que no sean de tipo cadena, se rechazan. Las claves desconocidas, las codificaciones malformadas y las infracciones de las restricciones hacen que la instancia se rechace antes de entregar los valores al código invitado.
Los valores canónicos de plugins.entries.<instance-key>.config del operador siguen siendo un mapa de cadenas marcado como secreto en memoria y se cifran al persistirse. El host deriva la clave de entrada versionada zpi1_… a partir de la identidad completa del paquete, la capacidad y el enlace; esto permite que distintos paquetes y ámbitos de capacidades reutilicen de forma segura alias como main. El manifiesto del paquete admitido selecciona el esquema. Un valor string se almacena directamente; los valores boolean, integer y number usan texto escalar de JSON como "true", "4" o "0.5"; los valores array y object usan texto JSON como '["urgent","ops"]' o '{"region":"us-east"}'. El host materializa y valida el conjunto completo de datos en JSON tipado para cada uso y, después, particiona cada propiedad exactamente una vez. Para una herramienta, los valores no secretos se inyectan bajo la clave reservada __config:
{
"prompt": "una puesta de sol",
"__config": {
"retry_limit": 4,
habilitado: true,
"etiquetas": ["urgente", "ops"]
}
}
La api_key omitida se lee explícitamente con secrets.get("api_key") si su esquema la marca como secreta. runtime.rs elimina cualquier __config proporcionado por el llamador antes de inyectar la sección pública, por lo que la sección no se puede falsificar. La inyección pública de la herramienta y las lecturas de secretos dentro de un mismo marco execute comparten una única revisión resuelta de la configuración activa; el marco se descarta tras un éxito, error, trampa, pánico o cancelación. La exportación configure de un canal no tiene ningún parámetro de configuración. Llama a config.get para obtener el objeto público y a secrets.get para las propiedades secretas, al igual que cada exportación operativa posterior que usa configuración. Ambas importaciones dentro de una misma llamada comparten una única revisión resuelta. El host descarta esa vista materializada tras un éxito, error, trampa, pánico o cancelación. Por lo tanto, los cambios en la configuración pública y las credenciales dentro de la misma vinculación lógica están disponibles conjuntamente en la siguiente operación cuando el huésped conforme resuelve ambos en el punto de uso.
Cuando el manifiesto solicita config_read, pero el host no lo concede efectivamente, la resolución sustituye el valor por un objeto vacío y valida ese objeto antes de que se ejecute el código invitado. Un esquema con campos obligatorios falla de forma segura durante la construcción. Si el objeto vacío es válido, las herramientas omiten el __config vacío, mientras que el canal config.get y secrets.get devuelven access-denied. Un complemento solo ve su propia sección.
Las exportaciones de metadatos estáticos del canal no pueden llamar a ninguno de los dos servicios de configuración y se leen una sola vez durante la carga. Por lo tanto, cambiar la identidad de un bot o una cuenta, o cualquier capacidad derivada de la configuración, identificador propio, mención o retardo entre mensajes, requiere reconstruir el ciclo de vida del canal; la configuración pública ordinaria y la rotación de credenciales para la misma vinculación lógica no lo requieren. Tool y channel son los consumidores actuales de la configuración. El mundo de memoria aún no tiene ninguna importación de configuración, por lo que los plugins de memoria no deben solicitar config_read hasta que se incorporen ese ABI y el cableado del runtime.
Egreso declarado ([egress])
Aplica a: cualquier plugin que solicite un permiso de transporte (http_client)
Un manifiesto puede incluir una tabla [egress] opcional que indica los destinos que el plugin declara necesitar:
[egress]
hosts = ["api.example.com", "*.cdn.example.com"]
Las entradas usan la gramática estricta de egreso: hosts exactos o patrones explícitos *.suffix, sin ninguna forma de permitir todo. La gramática no válida rechaza el manifiesto completo durante el descubrimiento y la instalación. La tabla forma parte de los bytes canónicos del manifiesto, por lo que un manifiesto firmado la cubre y cambiarla requiere volver a firmar.
Esto es una declaración de intención, nunca una concesión. La lista de permitidos que aplica el host es plugins.entries.<instance-key>.egress_hosts del operador, en la misma fila zpi1_… que contiene el mapa config privado de la instancia, resuelta a partir de la configuración activa en cada solicitud y no copiada como instantánea en el almacén. Un componente que incluya su propia tabla [egress] sin una concesión del operador no puede acceder a ningún destino. En esta versión, solo la concesión rige esa comprobación: la declaración no se intersecta adicionalmente con ella en el momento de la solicitud, por lo que declarar un host no lo concede ni limita una concesión que haya escrito el operador. Calcular la intersección de ambas es trabajo de implantación posterior, bajo #8850.
Lo que aporta la declaración es la ceremonia de instalación. zeroclaw plugin install la inserta en la fila [[plugins.entries]] que crea, de modo que la primera instalación de un plugin que declara sus destinos funciona sin que el operador tenga que transcribir los hosts, y los valores insertados se imprimen. Nunca amplía una fila que ya existe: una actualización cuya declaración creció imprime la diferencia junto con el comando zeroclaw config set exacto, y el operador lo aplica. zeroclaw plugin update no inserta nada, ni siquiera en una fila que crea, e imprime la misma diferencia. zeroclaw plugin list informa de la misma brecha como diagnóstico permanente. Declare los destinos que tu código realmente contacta, y trata una declaración que crece como algo que cada operador debe aprobar en cada actualización.
Una fila de instancia de canal se crea cuando un operador vincula su alias, con zeroclaw plugin bind <package> --channel-alias <alias> o con zeroclaw plugin install <source> --channel-alias <alias>, y la declaración solo se inserta en ella con un --egress declared explícito. --egress none vincula con una concesión vacía en su lugar, y un vínculo que crearía la fila sin ninguno de los dos flags se rechaza y muestra tus hosts declarados. Para una instancia de canal, la declaración cuenta con websocket_client o socket_client, además de http_client.
Si la reinstalación encuentra una fila de configuración anterior al tipado, no compatible, identificada por el nombre del paquete, la instalación se niega a continuar antes de crear la fila derivada zpi1_… e imprime los mismos pasos de actualización ordenados que plugin list. La instalación del paquete se revierte, y la configuración privada anterior y ambas listas de egress permanecen sin cambios hasta que el operador actualice la configuración beta y vuelva a intentarlo.
Los plugins cuyo destino es la configuración de despliegue (un Gitea autoalojado, un Nextcloud en la LAN) no deben declarar nada aquí. El operador crea esa concesión directamente en la fila de la instancia, y un manifiesto que no declara nada nunca se informa como si hubiera perdido destinos.
Host de componentes WASI
El host (crates/zeroclaw-plugins/src/component.rs) compila e instancia componentes contra un único wasmtime::Engine asíncrono. La forma en que se carga un archivo .wasm depende del backend de ejecución de la compilación:
plugins-wasm-cranelift: hay un backend JIT presente, así queload_componentcompila un componente.wasmal cargarlo medianteComponent::from_file.- Sin backend JIT (
plugins-wasm-pulleyo solo en tiempo de ejecución): no hay compilador en el binario, por lo queload_componentdeserializa el archivo directamente medianteComponent::deserialize_file, tratándolo como un.cwasmprecompilado producido por una wasmtime compatible. Un artefacto no coincidente es rechazado por la comprobación de versión de deserialize.
Ambas funciones del backend incorporan plugins-wasmtime; la ruta de carga se basa en si el compilador cranelift está en la compilación, no en pulley.
Límites de ejecución por llamada
Cada exportación del invitado se ejecuta con límites de recursos por llamada que el host aplica al store. El motor habilita la medición de combustible, y cada llamada recibe un presupuesto de combustible nuevo para que un componente descontrolado o malicioso provoque un trap en lugar de bloquear al host. El host también aplica un plazo de tiempo real al futuro completo de la exportación, incluido el tiempo de espera de importaciones asíncronas del host como wasi:http; las cesiones periódicas de combustible garantizan que el cálculo ininterrumpido del invitado no pueda impedir que se ejecute ese temporizador, y las importaciones del host accesibles desde el invitado nunca bloquean el ejecutor (los registros de log se entregan a una cola acotada y los escribe un hilo dedicado del host), por lo que el plazo sigue siendo observable mientras se ejecuta el trabajo del host. Un tope de StoreLimits limita la memoria lineal, los elementos de las tablas y el número de instancias. El mundo de herramientas obtiene un store nuevo en cada ejecución; los stores del canal persistente y de la memoria se reabastecen de combustible antes de cada llamada, de modo que un complemento de larga duración obtiene un presupuesto nuevo en lugar de agotarlo durante su vida útil.
Los cinco límites son ajustables por el operador, y cada valor se valida como distinto de cero: plugins.limits.call_fuel (valor predeterminado 1 000 000 000 unidades de instrucción), plugins.limits.call_timeout_ms (valor predeterminado 30 000 milisegundos), plugins.limits.max_memory_mb (valor predeterminado 256), plugins.limits.max_table_elements (valor predeterminado 100 000) y plugins.limits.max_instances (valor predeterminado 64). Un store solo puede construirse con límites explícitos, por lo que ninguna ruta de carga puede crear un plugin sin sandbox. Las opciones de solicitud wasi:http del guest pueden terminar una llamada antes, pero no pueden extender el plazo del host. Un store caliente interrumpido nunca se reanuda, y ni un canal ni un backend de memoria reutilizan un store cuya llamada falló dentro de Wasmtime: tras un trap del guest, un agotamiento de fuel o un fallo de un import del host, Wasmtime rechaza todas las llamadas posteriores al store, y un resultado que no pudo convertirse omite la limpieza posterior al retorno de la exportación. Los canales recrean la instancia a partir de entradas propiedad del host en la siguiente llamada; la reconstrucción espera cuando los traps y los plazos vencidos, excepto en los avisos de aprobación y de elección, superan un presupuesto de tres reconstrucciones simultáneas y, después de eso, una cada cinco minutos. Las instancias de memoria permanecen no disponibles hasta que su propietario las reconstruya, y las llamadas posteriores que lleguen al plugin fallan con el mismo error que deja un plazo vencido. Una instancia de memoria reconstruida empieza vacía, porque el mundo de memoria no da al plugin almacenamiento fuera de su propia instancia. Los campos canónicos y sus valores predeterminados se encuentran en la Referencia de configuración.
espacio de direcciones de 32 bits (wasip2 es wasm32)
El target del plugin es wasm32-wasip2, y el motor host está compilado con la medición de fuel habilitada (Config::consume_fuel(true)) sin wasm_memory64. El límite del plugin es un formato fijo de 32 bits, y eso tiene consecuencias que conviene decir claramente:
- El espacio de direcciones del invitado es de 32 bits. Un plugin se ejecuta en una memoria lineal wasm32. Los valores grandes cruzan el límite por valor:
media-attachmentde un plugin de canal transporta sus bytes completos como unlist<u8>, ywit/v0/channel.witya señala que esto puede ser de varios megabytes y deja un modelo de identificador de recurso para una revisión futura. Dentro de ese espacio de 32 bits, el host aplica un límite explícito de memoria por almacén desdeplugins.limits.max_memory_mb(predeterminado 256), así que un invitado queda limitado por el menor entre el espacio de direcciones wasm32 y ese límite configurado por ZeroClaw. - La ABI de componentes reduce los desplazamientos a 32 bits independientemente del tamaño de palabra del host. Incluso en un host de 64 bits, los desplazamientos de lista y cadena en la ABI canónica son
i32.memory64amplía el direccionamiento de memoria lineal de un invitado, no la ABI canónica del modelo de componentes, por lo que habilitarlo no haría que los campos a nivel de WIT fueran de 64 bits. - No existe ningún objetivo wasip2 de 64 bits contra el que enlazar.
wasm32-wasip2es el único objetivo de WASI Preview 2 en rustc y LLVM hoy; un plugin no puede compilarse como un componente p2 de 64 bits, así que no hay nada que el host pueda cargar aunque el motor habilitaramemory64.
Esta es una restricción de la cadena de herramientas upstream, no una limitación del host que un flag en este repo pueda levantar. Cuando un target p2 de 64 bits y una ABI de componente más amplia lleguen upstream, la costura bindgen! se regenerará contra ellos y los anchos de campo se revisarán en el WIT dentro de la ventana wit/VERSIONING.md. Hasta entonces, trate el límite del plugin como de 32 bits por construcción.
Firmas
Los manifiestos de plugins pueden incluir una firma Ed25519 (crates/zeroclaw-plugins/src/signature.rs). La firma está codificada en base64url a partir de los bytes canónicos del manifiesto (el TOML analizado, tras eliminar únicamente las entradas raíz exactas signature y publisher_key); la clave pública del editor está codificada en hexadecimal. Las propiedades anidadas del esquema con esos nombres siguen estando firmadas. El host aplica uno de los tres modos definidos en plugins.security.signature_mode:
| Modo | Plugin sin firmar | Firma no confiable o no válida |
|---|---|---|
strict | rechazado | rechazado |
permissive | cargado con una advertencia | cargado con una advertencia |
disabled | cargado | no verificado |
La verificación se ejecuta tanto en discovery como en install. Discovery omite un plugin que no cumple su política en lugar de abortar todo el host; install devuelve el error.
Escribir un plugin en Rust
Un plugin es un crate cdylib que apunta al modelo de componentes. Genera los bindings de invitado a partir del mismo paquete wit/v0 que usa el host, implementa el world exportado y compila para wasm32-wasip2. Para ver los recorridos completos desde un crate vacío hasta un plugin instalado, consulta las guías de plugins; las notas a continuación cubren la compilación y la instalación.
Construyendo
sh
# Instala el destino de WASI Preview 2 (una vez)
rustup target add wasm32-wasip2
# Compilar el componente
cargo build --target wasm32-wasip2 --release
El componente de salida está en target/wasm32-wasip2/release/<crate_name>.wasm. Cópialo junto a tu manifest.toml. Para una compilación del host solo en tiempo de ejecución sin backend JIT, precompila el componente a un .cwasm con un wasmtime compatible y distribúyelo en su lugar, ya que ese tipo de host deserializa en vez de compilar al cargarse.
Las pruebas de complementos de herramientas del host no dependen de un artefacto publicado: crates/zeroclaw-plugins/tests/fixtures/tool-fixture es un componente incluido en el árbol que se compila a partir del código fuente durante las pruebas, y reference_plugin.rs y reference_plugin_e2e.rs lo ejecutan mediante el mismo PluginHost, config_schema y las mismas rutas de resolución de configuración que utiliza el demonio. Si no se puede compilar el fixture, esas pruebas fallan.
Instalando
sh
# Copiar al directorio del complemento
zeroclaw plugin install /path/to/my-plugin/
# O manualmente
cp -r my-plugin/ ~/.zeroclaw/plugins/my-plugin/
Configuración
Los valores del operador actualmente se introducen a través del almacenamiento genérico de mapas de cadenas: edite [[plugins.entries]] en TOML, o use zeroclaw config set una vez que exista la entrada de la instancia. La instalación de una herramienta crea su entrada de vinculación predeterminada. Una clave de canal depende de su alias configurado, por lo que la entrada de una instancia de canal se crea cuando ese alias se vincula, mediante zeroclaw plugin bind <package> --channel-alias <alias> o mediante zeroclaw plugin install <source> --channel-alias <alias>. La construcción con reconocimiento de alias que resuelve la configuración tipada de un canal a partir de ese alias configurado se incorporó en #10146. zeroclaw plugin info <package> muestra cada clave que posee el paquete, para migración y ediciones posteriores: la del vínculo de la herramienta y una por cada alias vinculado, cuando la versión instalada posee estado de instancia y la instancia puede nombrarse con ese alias. Cada uno de esos alias vinculados también recibe el estado de preparación de esa instancia, con o sin línea de clave: si las claves obligatorias están establecidas o faltan, los veredictos de configuración y de salida (egress), y si el runtime la admitirá. Los formularios basados en esquema y la ayuda de campos en línea aún no están implementados. Las superficies actuales son:
- La CLI gestiona el ciclo de vida de los plugins con
list,search,install,bind,remove,infoymigrate.zeroclaw config setescribe valores individuales y sin procesar de los plugins; no interpreta el esquema del plugin. - zerocode puede editar la configuración estática del host de plugins de ZeroClaw, pero todavía no genera campos específicos para cada plugin a partir de
config_schema. - La puerta de enlace web es de solo lectura para los complementos:
GET /api/pluginsinforma de los complementos cargados y de si el sistema está habilitado. - El host valida
config_schemaal admitir el paquete y vuelve a validar/materializar los valores del operador antes de que el guest los use. - El esquema del manifiesto, para los autores de plugins, es el único contrato de tipos y validación en el límite del código invitado. Define allí cada clave compatible y cada restricción; no dupliques ese contrato en una estructura de configuración del tiempo de ejecución del host. El código invitado debe deserializar el JSON validado por el host en su estructura tipada nativa.
El esquema de configuración estática proporciona la ruta genérica de almacenamiento y marcado de secretos, no un editor dinámico por complemento. Los tipos de configuración de los complementos en crates/zeroclaw-config/src/schema.rs incluyen #[prefix = "plugins"], #[prefix = "plugins.entries"] y #[prefix = "plugins.security"], y la derivación Configurable convierte cada campo con prefijo en una ruta de configuración genérica. Los campos secretos (el mapa config de una entrada de complemento está marcado con #[secret]) se cifran en reposo mediante el .secret_key adyacente. Los campos canónicos, los valores predeterminados y los valores de signature_mode para la configuración del host se encuentran en la referencia de configuración; ese esquema es la fuente de verdad, mientras que cada manifiesto de complemento es la fuente de verdad para la estructura de su configuración privada.
Compilar funcionalidades
El host de complementos es una opción de activación en tiempo de compilación. Las características a nivel de binario en el Cargo.toml del espacio de trabajo seleccionan si los complementos se incluyen en la compilación en absoluto y qué backend de ejecución se distribuye:
plugins-wasmes el paraguas que incorpora el host de plugins y su integración en tiempo de ejecución al binario. Cada característica de backend que se describe a continuación lo implica, por lo que habilitar cualquier backend de ejecución (p. ej.,--features plugins-wasm-cranelift) siempre incluye el host de plugins y su superficie de CLI; una compilación exclusiva de backend no puede producir silenciosamente un binario sin el subcomandoplugin. El paraguas por sí solo equivale aplugins-wasm-runtime-only: sin JIT, por lo que solo se cargan componentes.cwasmprecompilados.plugins-wasm-runtime-onlyes el más pequeño y el más rápido de iniciar: no hay JIT, por lo que los componentes se deserializan desde un.cwasmprecompilado.plugins-wasm-craneliftañade el compilador Cranelift, por lo que un componente.wasmse compila al cargarse. Es la única función que incluye un compilador y la única que puede cargar los componentes.wasmque publica el registro de plugins.plugins-wasm-pulleycompila en el intérprete Pulley de wasmtime. ZeroClaw nunca selecciona Pulley como destino de compilación, por lo que en hosts con un backend nativo de Cranelift la característica no cambia nada: por sí sola se comporta comoplugins-wasm-runtime-only, y junto conplugins-wasm-craneliftcompila código nativo. En hosts sin backend nativo, como ARM de 32 bits, wasmtime apunta a Pulley por sí mismo.
Estos delegan en las características del crate zeroclaw-plugins (plugins-wasmtime, plugins-wasm-cranelift, plugins-wasm-pulley) que conectan wasmtime. La ruta de carga se basa en si el compilador Cranelift está en la compilación, como se describe en WASI Component Host. Lee los comentarios de las características en el Cargo.toml del espacio de trabajo para las descripciones autorizadas.
Soporte de plugins por artefacto de lanzamiento
El conjunto de funciones de distribución estándar (cargo generate features --selection dist, resuelto por destino desde el registro en el Cargo.toml del workspace) incluye plugins-wasm-cranelift en los siete destinos de escritorio y servidor de 64 bits, y lo excluye en ARM de 32 bits y en el destino experimental de Android. ¿Qué artefacto contiene el host y qué lo demuestra:
| Artefacto | Host de plugins | Paquete aceptado | Prueba |
|---|---|---|---|
| Archivos CLI para Linux x86_64 y aarch64 (glibc y musl), macOS aarch64 y x86_64, Windows x86_64 | Compilador Cranelift, .wasm compilado al cargar | .wasm componente compilado contra wit/v0 de la versión. | la release ejecuta un plugin de cada archivo en su propia plataforma (el trabajo plugin-smoke) antes de que se ejecuten publish y docker |
| Archivos CLI para Linux ARMv7 y arm (ARMv6) | ninguno: plugin no es un comando | ninguno | la política por destino del registro, fijada mediante una prueba de xtask |
| Archivo CLI para aarch64-linux-android (experimental) | ninguno | ninguno | I don’t have a string to translate yet. Please paste the English text you want translated into Spanish. |
Imágenes de GHCR latest, con versión y debian | Cranelift: ellos reempaquetan los archivos de glibc para Linux x86_64 y aarch64 | .wasm | hereda la prueba de humo de archivo; no se ejecuta nada dentro de la imagen |
Variante dist de GHCR, paquete AUR, paquete de Nix flake, compilación desde código fuente con setup.bat | Cranelift: compilación desde el código fuente a partir de la lista de distribución generada | .wasm | ninguno |
Imagen all-features de GHCR | Cranelift, Pulley y runtime-only, todos compilados | .wasm | ninguno |
Imágenes minimal y default-features de GHCR | ninguno | ninguno | |
| Paquetes de escritorio (dmg, deb, AppImage, msi, setup exe) | Cranelift: la compilación estándar del sidecar usa las características dist generadas para el destino (más embedded-web en macOS) | .wasm | Conexión de origen en scripts/desktop/prepare-kernel.sh; los paquetes de escritorio no tienen una puerta de prueba de humo para plugins |
cargo install zeroclaw y la compilación desde el código fuente con install.sh | Ninguno por defecto; actívalo con --features plugins-wasm-cranelift | .wasm con la opción de activación voluntaria | |
| Homebrew core | propiedad de la fórmula, fuera de este repositorio |
Límites que se aplican a todos los artefactos del host:
wit/v0es inestable. Un plugin debe compilarse contra el WIT incluido en la versión que lo ejecuta (wit/VERSIONING.md); el manifiesto no contiene versión del host ni de WIT, por lo que una discrepancia se detecta en la comprobación de carga durante la instalación.- Compilar un componente necesita memoria que se vuelve ejecutable después de escribirse. Los entornos que lo prohíben, como las unidades de systemd con
MemoryDenyWriteExecute=yeso las denegaciones de SELinux porexecmem, rechazan todas las cargas de plugins mientras el resto del binario funciona con normalidad. - Los paquetes oficiales del registro no están firmados actualmente, por lo que con
signature_mode = "strict"ninguno de ellos se instala. - Cada llamada a una herramienta vuelve a compilar el componente; no hay caché de compilación.
- Los valores predeterminados en tiempo de ejecución permanecen cerrados. Una herramienta o un paquete de skills instalado no aporta nada hasta que el operador establece
plugins.enabledyplugins.auto_discover. Una vez establecidos ambos, cada compilación del registro de herramientas compila e instancia cada componente de herramienta admitido y consulta su nombre, descripción y esquema de parámetros según los permisos y la política de salida (egress) declarados por el paquete; la aprobación solo controlaexecute. Un paquete de canal se ejecuta una vez queplugins.enabledestá establecido, el operador lo declara en[channels.plugin.<alias>]y un agente habilitado lo incluye entre sus canales comoplugin.<alias>. Para un paquete que replica un canal nativo medianteprovides, la activación bajo[channels.<provides>.<alias>]se registra, pero aún no se construye. Cada llamada a una herramienta de plugin sigue necesitando la aprobación del operador, salvo que el perfil de riesgo del agente la pre-apruebe (auto_approve, incluido el comodín*, olevel = "full") o que el operador haya respondido Siempre para esa herramienta antes en la misma sesión de CLI o turno del canal. Las herramientas enalways_asksiempre solicitan confirmación.
Recuperación de una instalación interrumpida
zeroclaw plugin remove <name> puede recuperar un directorio de paquete no cargado, vacío o estructuralmente incompleto que contenga únicamente lo que escribe una instalación: un manifiesto que nombra ese paquete, el archivo de componente en wasm_path con las carpetas de su ruta y, para un plugin de habilidades, un árbol skills/ de carpetas y archivos. Los rechazos por política de firma, los paquetes sanos, los alias cargados, los enlaces simbólicos, los contenidos no inspeccionables y los directorios que contengan cualquier otra cosa siguen protegidos. La recuperación reclama una única generación del directorio antes de la admisión y la eliminación; nunca usa un veredicto obsoleto para eliminar recursivamente un reemplazo en el nombre del paquete.
La instalación y la recuperación se coordinan mediante un bloqueo persistente y oculto del sistema operativo bajo la raíz de plugins. Solo la instalación, la actualización, la eliminación y el listado, así como lo que una actualización interrumpida desplazó, abren esa raíz y su bloqueo; por tanto, un host que solo descubre plugins, como uno que mantiene el daemon, no retiene nada en el directorio de plugins. En Windows, una raíz abierta impide renombrar el directorio de plugins y sus antecesores hasta que el host la libere. Un proceso que no pueda obtener el bloqueo en 60 segundos desiste e indica el nombre del archivo de bloqueo. Cualquier cuenta que pueda leer ese archivo puede mantener el bloqueo. Cuando el sistema operativo o el sistema de archivos no ofrezcan bloqueo de archivos, las operaciones de instalación, actualización y eliminación se rechazan. Los nuevos directorios de staging usan generaciones únicas y una concesión (lease) retenida por el sistema operativo, en lugar de un PID como evidencia de propiedad. La recuperación limpia una etapa de protocolo abandonada solo al recuperar un paquete final real, o al terminar de eliminar uno que una eliminación anterior había determinado. Las etapas heredadas que solo tienen PID y no cuentan con prueba de abandono se conservan y sus rutas se imprimen; no impiden que una nueva instalación use staging nuevo.
Un paquete reclamado y rechazado se restaura sin sobrescribir a un ocupante concurrente. Si la restauración no puede completarse, el error indica la ubicación de su transacción oculta retenida. Conserve ese directorio, resuelva el conflicto informado y vuelva a ejecutar plugin remove: el reintento restaura el paquete antes de aplicar la política normal de admisión. Una reclamación cuya eliminación ya había comenzado queda marcada, y un reintento completa esa eliminación en lugar de restaurarla. Una transacción oculta nunca es, por su nombre por sí solo, prueba de que un paquete sano pueda eliminarse. Los cambios en el espacio de nombres del sistema de archivos y el bloqueo no compatible rechazan la recuperación de forma segura. Los traslados de paquetes requieren un renombrado atómico que rechace todo destino existente, incluido un directorio vacío. Cuando la plataforma o el sistema de archivos no pueden proporcionarlo (otras plataformas Unix como FreeBSD, un montaje NFS sin soporte de no reemplazo, o Linux anterior a la 3.15), la instalación y la recuperación rechazan el traslado. Una reclamación fallida deja la entrada final sin mover. Una instalación rechazada de este modo elimina la copia que preparó en staging e indica que el traslado no es compatible; cualquier otra publicación fallida, así como una restauración fallida, conserva el paquete en su transacción oculta e informa esa ruta. No se usa ningún renombrado simple con reemplazo.