Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Complementos

El sistema de complementos de ZeroClaw te permite añadir capacidades al agente sin tocar el binario principal. Esta página explica la decisión tecnológica: de qué está hecho un complemento, por qué es WebAssembly y cómo el host mantiene contenido un componente no confiable. Las guías de abajo recorren la creación de cada tipo de complemento, volviéndose más técnicas a medida que bajas.

Los paquetes de habilidades en Markdown-only no son plugins, pero viajan a través de la misma maquinaria de manifiesto, firma e instalación; esa página vive junto con la documentación de Skills.

Para la vista del operador sobre el descubrimiento, la política de firma y la configuración, consulte Cómo funcionan los complementos. Para la referencia normativa del contrato, consulte Protocolo de complementos.

Por qué WebAssembly

Un plugin ejecuta código arbitrario de terceros dentro de un proceso que contiene tus claves de API, tu historial de conversaciones y acceso al shell. El límite de aislamiento tiene que ser real, no meramente consultivo. ZeroClaw usa el modelo de componentes WASI sobre wasmtime porque ofrece cuatro propiedades que ningún esquema de biblioteca dinámica o subproceso iguala a la vez:

  1. Aislamiento basado en capacidades. Un componente WebAssembly no tiene autoridad ambiental. No puede abrir archivos, sockets ni variables de entorno a menos que el host conecte explícitamente esa capacidad en su enlazador. El host de ZeroClaw construye cada almacén de plugins con un contexto WASI que no tiene preaperturas del sistema de archivos ni red (PluginState en crates/zeroclaw-plugins/src/component.rs). Lo que un plugin puede alcanzar es exactamente el conjunto de importaciones del host que declara su world, más lo que añadan los permisos de su manifiesto, y nada más.
  2. Ejecución con medición de combustible. El motor está compilado con la medición de combustible habilitada, y cada llamada recibe un presupuesto de combustible nuevo y un plazo basado en el reloj de pared que incluye el trabajo del host cuyo resultado se espera. Un complemento que entra en un bucle infinito o espera indefinidamente falla; no puede bloquear al agente. Los límites de memoria, tablas e instancias se aplican mediante un limitador del almacén. Los cinco límites proceden de la configuración del operador (plugins.limits.*) y se validan para que no sean cero, y no se puede construir un almacén sin ellos, por lo que ninguna ruta de carga puede producir un complemento sin aislamiento.
  3. Una ABI tipada e independiente del lenguaje. El contrato entre el host y el plugin es un conjunto de archivos de interfaz WIT (wit/v0/ en el repositorio de ZeroClaw), no una API de Rust. El host genera sus enlaces a partir de esos archivos con bindgen! de wasmtime; un plugin genera los enlaces de invitado en imagen especular con wit-bindgen en Rust o la herramienta equivalente en cualquier lenguaje que compile a un componente wasm32-wasip2. Los registros, variantes, resultados y tipos opción cruzan el límite con sus tipos intactos.
  4. Comportamiento idéntico al de los integrados. Cada tipo de plugin se adapta al mismo trait de Rust que usan las implementaciones propias: un plugin de herramienta se convierte en un Tool (wasm_tool.rs), un plugin de canal en un Channel (wasm_channel.rs), un plugin de memoria en un Memory (wasm_memory.rs). El bucle del agente, la atribución, los recibos y la política de seguridad no ven ninguna diferencia.

Las piezas

Un complemento en disco es un directorio que contiene un manifiesto y un componente compilado:

~/.zeroclaw/plugins/
└── my-plugin/
    ├── manifest.toml     # identity, capabilities, permissions, signature
    └── my-plugin.wasm    # wasm32-wasip2 component

El manifiesto declara dos cosas ortogonales:

  • Capacidades: lo que el plugin es. Una o más de tool, channel, memory, observer, skill (el enum PluginCapability en crates/zeroclaw-plugins/src/lib.rs). Cada capacidad WASM selecciona el mundo WIT que el componente debe exportar. La capacidad skill es la excepción: marca un paquete markdown skill bundle que utiliza la maquinaria de instalación, no código, y no necesita ningún componente.
  • Permisos: a qué servicios del host puede acceder el código del plugin. El enum PluginPermission está en el mismo archivo. Actualmente, config_read (los adaptadores de herramienta y de canal reciben su propia configuración pública, materializada y validada según el esquema, y pueden resolver secretos designados por el esquema en llamadas de servicio autorizadas) y http_client, state_read y state_write tienen efecto de comportamiento. El permiso HTTP es la concesión necesaria para los adaptadores que implementan wasi:http saliente: los adaptadores de herramienta y de canal habilitan esa superficie, mientras que el de memoria intencionadamente aún no lo hace. Los permisos de estado controlan el acceso al estado duradero cifrado que pertenece exactamente al paquete, la capacidad y el enlace. config_read debe emparejarse con el config_schema del manifiesto; si se declara uno sin el otro, se rechaza. Los permisos de sistema de archivos y de acceso a memoria son aceptados por el esquema, pero aún no están respaldados por funciones del host, por lo que declararlos no concede nada.

Los mundos

wit/v0/ define un mundo por capacidad WASM. Cada mundo importa la interfaz de host logging, cuyos eventos log-record llegan al log estructurado que lleva la atribución de span del sitio de llamada del host, y exporta plugin-info (nombre y versión autoinformados) además de su interfaz principal:

WorldExportacionesCiclo de vida del almacén
tool-plugintool: nombre, descripción, esquema de parámetros, executeAlmacén nuevo por cada execute; importaciones con ámbito en secrets y state duraderos
channel-pluginchannel: configurar, enviar, poll-message, además de 23 métodos condicionados por capacidadesAlmacén en caché protegido por un mutex asíncrono, reabastecido en cada llamada; importa config, secrets, state duradero e inbound alimentado por el host, todos con ámbito limitado.
memory-pluginmemory: almacenar, recuperar, obtener, olvidar, más 11 métodos limitados por capacidadAlmacén cálido detrás de un mutex asíncrono, reabastecido en cada llamada

Los mundos de channel y memory usan banderas de capacidad: una máscara de bits que el host lee una sola vez al cargar (get-channel-capabilities / get-memory-capabilities). Para cada bandera no establecida, el host usa el valor predeterminado del trait de Rust y nunca llama al export del plugin. Así es como el contrato WIT se mantiene aditivo: un nuevo método opcional es una nueva bandera más una nueva función, nunca una ruptura.

Comprobando que un plugin sigue cargándose aquí

wit/v0 es experimental, así que un plugin compilado contra una copia vendorizada que se ha desfasado de este host compila sin errores y luego falla al instanciarse. zeroclaw plugin install rechaza un plugin así, pero esa comprobación no sirve de nada para uno instalado antes de que existiera, instalado con --no-verify, o que se dejó en su sitio tras actualizar el host. zeroclaw plugin info <name> siempre ejecuta la misma comprobación de instanciación que el daemon ejecuta al arrancar, e imprime el veredicto, la cadena completa de causas de wasmtime y la sugerencia de recompilación; sale con un código distinto de cero cuando el plugin no se carga, para que un script pueda decidir según el resultado. zeroclaw plugin list --verify ejecuta la comprobación para cada paquete instalado y anota cada fila con el veredicto, o con la primera línea de la causa cuando falla. zeroclaw plugin list sin opciones no cambia y sigue costando solo una lectura de directorio, porque verificar compila e instancia cada componente. Un paquete de tipo skill no incluye ningún componente, así que se informa como no aplicable en lugar de como fallo.

Recuperación de una instalación incompleta

zeroclaw plugin install crea un paquete en una transacción oculta única dentro del directorio de plugins y mantiene un bloqueo (lease) del sistema operativo hasta la publicación. Mueve el paquete a su ubicación final sin reemplazar ningún elemento existente, solo después de escribir el manifiesto, el componente y, si existe, el árbol skills/. Si la plataforma o el sistema de archivos no puede ofrecer un movimiento atómico sin reemplazo, como ocurre en FreeBSD y en los demás sistemas Unix distintos de Linux, Android y macOS, en un montaje NFS sin soporte para ese movimiento, o en Linux anterior a la 3.15, la instalación se rechaza, elimina su copia de preparación y lo indica explícitamente; reintentarlo en esos casos produce el mismo fallo. Una escritura fallida o interrumpida no deja ningún paquete parcial con el nombre final. El descubrimiento ignora las transacciones ocultas, y una instalación posterior usa una preparación nueva en lugar de reemplazar la preparación de otro proceso.

Un movimiento no compatible no es motivo para reintentar en el mismo sistema de archivos no compatible. Si la limpieza también falla, la instalación informa de la ubicación de limpieza que falló y exige que se resuelvan primero ese fallo y el soporte de movimiento seguro. La ubicación es el directorio del paquete en caso de vaciado parcial o de fallo al desvincular un directorio vacío, o la entrada de la transacción si su paquete no existe o ya se eliminó. El mensaje no afirma que la carga útil original siga intacta. Las comprobaciones de limpieza tratan una raíz de plugins o un bloqueo de coordinación reemplazado o eliminado como un rechazo de espacio de nombres antes de continuar. La transacción oculta notificada pertenece a esta instalación fallida y no bloquea una instalación posterior: plugin remove no la elimina mientras no exista ningún paquete con ese nombre, así que, cuando no haya ninguna instalación en ejecución, puedes inspeccionar y eliminar manualmente esa transacción específica.

Las compilaciones anteriores escribían directamente en el directorio final: lo creaban y luego escribían manifest.toml primero, por lo que una instalación interrumpida podía dejar un directorio vacío, un manifiesto truncado o un manifiesto sin su componente. El descubrimiento omite ese directorio, por lo que no es un paquete instalado, pero aun así reserva el nombre. zeroclaw plugin install nunca sobrescribe nada que ya exista con un nombre de paquete: se niega y indica zeroclaw plugin remove <name> como recuperación.

Para un nombre que el host no ha cargado, plugin remove reclama la generación de directorio seleccionada en una transacción de recuperación oculta y aplica las mismas comprobaciones de admisión a esos contenidos retenidos. Solo elimina esa generación en dos casos:

  • El directorio está vacío.
  • Contiene un manifest.toml y no supera las comprobaciones de admisión que aplican install y discovery apply, debido a su propio contenido: un manifiesto truncado o que no se puede analizar, un componente que falta, un componente que no coincide con su wasm_sha256 declarado, o un paquete que este host no puede aceptar tal como está escrito, como un componente que supera el límite de tamaño de admisión, un config_schema que no puede compilar, o un paquete de skills incompleto. Además, no debe contener nada que una instalación nunca escriba. Una instalación escribe un paquete bajo el nombre de su manifiesto: primero el manifiesto y, después, solo el componente en su wasm_path, como un archivo con las carpetas intermedias necesarias y, para un plugin de skills, una carpeta skills/ con archivos y subcarpetas. Por tanto, junto a un manifiesto que no se puede analizar no puede haber nada más, y junto a uno que sí se analiza solo pueden existir esas entradas, bajo el nombre que indica ese manifiesto.

La recuperación también limpia las fases abandonadas de forma demostrable, creadas por el protocolo de concesión. Las fases activas y los directorios heredados ambiguos .<name>.installing-<pid> se conservan, y el comando informa de sus rutas. Un PID en un nombre de archivo no demuestra que la fase esté abandonada. Estas fases conservadas no impiden una instalación nueva una vez que se recupera el paquete final incompleto. Si no existe ningún paquete final, plugin remove no elimina los directorios de staging, salvo que complete una eliminación que un plugin remove anterior había iniciado.

Los demás ocupantes son rechazados, y el comando muestra el motivo: un enlace simbólico o un archivo con ese nombre, un directorio que contiene archivos pero no manifest.toml, uno que contiene archivos que una instalación nunca escribe, como la copia de código fuente de un plugin cuyo componente aún no está compilado o un .DS_Store que añadió un gestor de archivos, uno cuyo manifiesto nombra otro paquete, un directorio que no puede inspeccionar ni listar, el directorio desde el que se cargó un paquete en uso, un paquete que la admisión acepta, y un paquete que este host rechaza por su política de firmas (sin firmar, de un editor no confiable, con una firma inválida, o sin un wasm_sha256 firmado en modo strict). La admisión comprueba la firma antes de leer el componente, así que en modo strict un manifiesto huérfano de un paquete sin firmar, o uno recortado de forma que su firma ya no se verifica, es rechazado por su firma aunque su componente falte.

Un paquete reclamado y rechazado se restaura sin reemplazar a ningún ocupante concurrente, incluido un directorio vacío. La recuperación rechaza una reclamación o restauración cuando la plataforma o el sistema de archivos no puede proporcionar un movimiento atómico sin reemplazo. Si la restauración no puede completarse, sus bytes permanecen en la ubicación oculta indicada; el nombre original no necesariamente permanece intacto. Reintentar plugin remove restaura una transacción de recuperación retenida antes de volver a comprobar la admisión. En lugar de eso, se termina una eliminación que plugin remove había iniciado: una vez que comienza a eliminar un paquete que consideró incompleto, un reintento completa esa eliminación. Resuelva el conflicto de destino o el error del sistema de archivos notificado antes de reintentar; no elimine transacciones ocultas solo porque parezcan restos de operaciones anteriores.

Modelo de ejecución

El host (crates/zeroclaw-plugins/src/component.rs) posee un único wasmtime::Engine asíncrono para el proceso. La carga depende del backend: una compilación con el JIT de Cranelift compila .wasm al cargarse; una compilación solo en tiempo de ejecución deserializa un .cwasm precompilado. Cada instanciación de plugin obtiene:

  • un Store que contiene el contexto WASI aislado, la tabla de recursos, el contexto HTTP opcional y el presupuesto de combustible;
  • un Linker con exactamente las importaciones que requieren su mundo, sus permisos y el soporte de adaptadores incluye logging siempre, secrets para herramientas y canales, config e inbound para canales, y wasi:http para adaptadores de herramientas y canales solo cuando el manifiesto concede http_client. Memory no crea ni un contexto HTTP ni un enlazador HTTP. Cada adaptador comprueba la coherencia entre su contexto y su enlazador al instanciarse (ensure_http_coherent).

Las llamadas a herramientas son stateless por construcción: WasmTool::execute crea un store nuevo, ejecuta la llamada y lo descarta. Los canales y los backends de memoria son stateful por naturaleza, así que mantienen un store caliente durante toda la vida del plugin. El host reabastece su combustible antes de cada llamada, de modo que un plugin de larga duración recibe un presupuesto completo por llamada en lugar de agotarlo con el tiempo. Una interrupción por plazo vencido descarta el store caliente en lugar de reanudar un estado del guest parcialmente desenrollado. Los canales y los backends de memoria también descartan un store cuya llamada falló dentro de Wasmtime: Wasmtime rechaza todas las llamadas posteriores a un store tras un trap, un combustible agotado o un fallo en un import del host, y un resultado que no pudo deserializarse omite la limpieza de la export. Los canales recrean la instancia en la siguiente llamada y esperan cuando los fallos superan el presupuesto de reconstrucción. La memoria permanece no disponible hasta que su propietario la reconstruya, y cada llamada que llegue al plugin mientras tanto falla con el mismo error plugin instance is unavailable. Un plugin de memoria conserva sus entradas solo en su propia instancia, por lo que una instancia reconstruida empieza vacía. Durante una llamada de canal autorizada, config.get y secrets.get materializan como máximo una revisión de la configuración canónica de esa instancia admitida. El host descarta la vista cuando termina la llamada. Un plugin de canal conforme debe resolver ambos en cada punto de uso y no debe retener en el estado caliente del guest la configuración devuelta ni el secreto en texto plano. El host no puede garantizar la no retención después de entregar los datos a código guest de confianza.

El límite es de 32 bits: wasm32-wasip2 es el único destino de WASI Preview 2 que incluye la toolchain de Rust, y la ABI de componentes reduce los offsets a 32 bits independientemente del tamaño de palabra del host. Los valores grandes (los bytes de un adjunto de canal) cruzan por valor. Consulta la página del protocolo para ver por qué esta es una restricción upstream.

Estado actual del cableado

Tenga en cuenta qué está registrado de extremo a extremo frente a qué está completo en el host pero aún no es accesible desde un daemon en ejecución:

CapacidadAdaptador hostCableado en tiempo de ejecución
toolWasmToolRegistrado de extremo a extremo; los complementos de herramientas descubiertos aparecen en el conjunto de herramientas del agente
skillcargador de MarkdownRegistrado de extremo a extremo; las habilidades se cargan con espacio de nombres como plugin:<plugin>/<skill>
channelWasmChannel, completo y cubierto por pruebas unitariasLa construcción gestionada por el alias y la resolución de la configuración en tiempo de ejecución ya se incorporaron (#10146); el escuchador de host específico de cada proveedor que vuelca cada transporte en la cola inbound del canal queda para una tarea posterior
memoryWasmMemory, implementa el trait completo MemoryEl runtime aún no lo construye como un backend configurable
observerningunoPluginCapability::Observer está reservado; todavía no existe ningún mundo WIT ni adaptador

Configuración

La configuración estática del host de plugins usa el mismo espejo del esquema que todo lo demás. Actualmente, los valores por instancia usan TOML genérico o zeroclaw config set; el esquema del manifiesto del plugin todavía no se representa como un formulario de zerocode o gateway. Ten cuidado al editar manualmente: un error de sintaxis en una sección (por ejemplo, [plugins.entries] cuando debería usarse [[plugins.entries]]) hace que actualmente toda la sección [plugins] no se pueda deserializar y se revierta silenciosamente a los valores predeterminados, que se vuelven a leer como plugins.enabled = false sin ninguna advertencia (registrado en la incidencia #8636). Las operaciones habituales:

# encienda el sistema
zeroclaw config set plugins.enabled true

# carga durante la ejecución los complementos de herramientas y habilidades detectados automáticamente (predeterminado: false)
zeroclaw config set plugins.auto_discover true

# donde se descubren los plugins (por defecto: ~/.zeroclaw/plugins)
zeroclaw config set plugins.plugins_dir /srv/zeroclaw/plugins

# política de firma: disabled | permissive | strict
zeroclaw config set plugins.security.signature_mode strict

# límites de sandbox por llamada
zeroclaw config set plugins.limits.call_fuel 1000000000
zeroclaw config set plugins.limits.call_timeout_ms 30000
zeroclaw config set plugins.limits.max_memory_mb 256

plugins.limits.max_connections_per_instance (predeterminado 16) limita cuántas conexiones salientes puede mantener abiertas a la vez una misma instancia lógica de plugin. El límite se aplica por instancia, no por llamada: una respuesta mantiene su conexión hasta que el guest haya consumido el cuerpo o descartado la respuesta, así que una herramienta que mantiene dieciséis respuestas activas dentro de una misma invocación no puede abrir una decimoséptima conexión. Las solicitudes secuenciales que consumen o descartan cada respuesta a medida que avanzan nunca se acercan al límite. Cuando el límite se hace efectivo, el guest recibe el error propio de wasi:http, connection-limit-reached, y no una denegación de salida: el destino fue autorizado, y el host informa de un recurso que él mismo contabiliza.

plugins.enabled = true activa el host de plugins, pero las capacidades de herramientas y habilidades detectadas automáticamente solo se cargan cuando plugins.auto_discover = true también está establecido. Esa marca es false de forma predeterminada (cerrada ante fallos), por lo que enabled = true por sí solo te proporciona los canales que declares en [channels.plugin.<alias>] y ninguna herramienta ni habilidad de plugin: un paquete de herramientas o habilidades puede listarse y mostrar info correctamente, pero no aportar nada en tiempo de ejecución. Las vinculaciones de canales explícitas reciben su nombre del operador en lugar de detectarse automáticamente, por lo que no necesitan auto_discover; la marca solo controla las herramientas y habilidades detectadas automáticamente.

Los ajustes por instancia se almacenan bajo plugins.entries, indexados por una cadena versionada zpi1_… derivada de la identidad de paquete, capacidad y enlace que gestiona el host. La instalación muestra y siembra la clave del enlace de herramienta predeterminado del paquete. El enlace de una instancia de canal es su alias configurado, no un nombre de paquete inventado, por lo que su clave existe solo cuando se vincula un alias: zeroclaw plugin bind <package> --channel-alias <alias> o zeroclaw plugin install <source> --channel-alias <alias> crean el enlace [channels.plugin.<alias>] y siembran la fila de esa instancia (véase Vincular una instancia de canal). Un daemon resuelve la configuración tipada de una instancia [channels.plugin.<alias>] declarada explícitamente a través de esa misma clave, la ruta de ejecución incorporada en #10146. zeroclaw plugin info <package> muestra todas las claves que posee el paquete: la de su enlace de herramienta y una por cada alias vinculado, cuando la versión instalada gestiona estado por instancia y una instancia puede identificarse con ese alias. El informe de disponibilidad de un alias vinculado, que puede identificar una instancia de este tipo, sigue el mismo criterio en ambos casos. Las claves con identidad completa permiten que distintos paquetes y mundos de capacidades reutilicen alias como main sin compartir credenciales. Los valores canónicos del operador son un mapa de cadenas marcado como secreto y permanecen cifrados en reposo (enc2:…). Un plugin que solicita config_read declara el contrato de tipo único del mapa en config_schema: un objeto cerrado de Draft 2020-12 cuyas propiedades de nivel superior usan explícitamente string, boolean, integer, number, array u object. Un consumidor de herramienta o de canal puede establecer x-secret = true en una propiedad de cadena de nivel superior; el host valida el objeto completo, la elimina de la configuración pública y la pone a disposición únicamente mediante la importación secrets.get de la instancia admitida. Las herramientas pueden leer secretos durante execute; los canales obtienen la configuración pública mediante config.get y los secretos mediante secrets.get durante configure y en las llamadas operativas. Sin el permiso config_read efectivo, cualquiera de las dos importaciones devuelve access-denied; la instanciación, el descubrimiento de metadatos estáticos, el fallo de resolución y el agotamiento del presupuesto de llamadas al host devuelven unavailable. Almacene las cadenas directamente, el texto de escalares JSON para booleanos y números, y el texto JSON para arrays y objetos. El host materializa y valida el objeto tipado resultante antes de usar el código invitado de la herramienta o del canal; los valores desconocidos, mal formados o fuera de rango fallan en lugar de llegar al plugin. Los plugins de memoria todavía no tienen una importación de configuración y no deben solicitar config_read hasta que se añada ese ABI.

Los componentes de herramientas y de canal pueden solicitar state_read y/o state_write para la importación state. El host, no el invitado, proporciona el paquete admitido, la capacidad y el espacio de nombres de vinculación. Las claves lógicas portables direccionan valores de bytes autenticados dentro de esa instancia exacta. Las lecturas devuelven una revisión; las operaciones de creación, reemplazo y eliminación usan revisiones exactas de compare-and-swap. Las filas duraderas se almacenan como texto cifrado enc2: detrás de índices ciegos con clave en data/plugin-state.db, usando el .secret_key de la instalación. Las cuotas fijas y cualquier fallo de clave, almacenamiento o integridad fallan en modo cerrado.

El estado pertenece a la identidad de la instancia (nombre del paquete, capacidad y vinculación), la misma identidad que usa su entrada de configuración, y no a un publicador. zeroclaw plugin update conserva ambas, al igual que zeroclaw plugin remove, por lo que una actualización mantiene su estado tanto si se trata de una actualización como de una eliminación seguida de una instalación. También significa que un paquete distinto instalado después con el mismo nombre hereda el estado de esa instancia y sus secretos configurados. La eliminación conserva todas las vinculaciones [channels.plugin.<alias>] que mencionan el paquete, de modo que el nuevo paquete queda vinculado a esos alias y a sus filas también. plugin remove muestra cada vinculación que dejó atrás y cada fila conservada que todavía concede acceso de salida, indicando qué eliminar: cuando la versión instalada proporciona un canal, la fila bajo la clave de cada alias vinculado; cualquier otra cosa que esa versión posea; y la fila de la vinculación de herramienta predeterminada cuando la versión instalada posee estado de instancia. Antes de instalar un plugin no relacionado con el nombre de un plugin eliminado, borra sus filas [[plugins.entries]] y esas vinculaciones, y considera que su estado es legible por el nuevo paquete.

Los autores de plugins anteriores a la versión 1.0 deben migrar explícitamente: un manifiesto que solicita config_read sin config_schema ya no se detecta. Añada un esquema cerrado que coincida con los valores actuales, actualice los invitados de herramientas o canales para deserializar JSON tipado en lugar de un mapa de cadenas, vuelva a compilar y a firmar porque el esquema está incluido en la firma. Las integraciones del host inyectan PluginHostServices, que encapsula un PluginConfigResolver, en lugar de un mapa de configuración propio. Cada marco autorizado de herramienta o canal materializa como máximo un ResolvedPluginConfig vinculado al ámbito, usa esa vista para cada lectura de configuración dentro del marco y la descarta cuando este finaliza. Un canal llama a config.get y secrets.get en el punto de uso, de modo que la configuración pública y la rotación de credenciales dentro del mismo vínculo lógico se ven como una sola revisión en la siguiente operación. Las exportaciones de identidad estática y capacidades se leen una sola vez durante la carga; cambiar la identidad del bot o de la cuenta, u otros metadatos estáticos, requiere reconstruir el ciclo de vida del canal. Migración a la configuración tipada es la receta paso a paso, incluida la decisión de aplicar esta exigencia sin una capa de compatibilidad.

Este es un formato de clave estricto anterior a la versión 1.0: no se consultan las entradas heredadas cuyo nombre solo coincide con el de un paquete o binding. Para un paquete de herramientas existente, ejecuta zeroclaw plugin info <package> para obtener su clave de instancia completa, cambia el nombre de la entrada antigua a esa clave y guarda la configuración. Las instalaciones nuevas de herramientas la inicializan automáticamente.

Los permisos efectivos se comprueban por separado de las solicitudes del manifiesto. Si se deniega config_read, el host valida un objeto vacío. Las propiedades obligatorias hacen que el inicio falle de forma segura. Cuando el objeto vacío es válido, una herramienta omite la clave vacía __config y las importaciones de configuración/secretos del canal devuelven access-denied. La lista canónica de campos y los valores predeterminados del host se encuentran en la Referencia de configuración; zeroclaw config list muestra los valores almacenados actuales.

Declarar y conceder el egreso

A plugin’s alcance de red son dos hechos separados, y solo uno de ellos es tuyo:

  • La declaración es la tabla [egress] del manifiesto (hosts = [...]): los destinos que el autor indica que el plugin necesita. Forma parte de los bytes canónicos del manifiesto, por lo que un manifiesto firmado la cubre. No concede nada. Un componente sin firmar que escriba su propia tabla [egress] tampoco alcanza ningún destino.
  • El permiso es plugins.entries.<instance-key>.egress_hosts en la fila zpi1_… propia de la instancia, junto con la excepción más restringida egress_allow_private. Esta es la lista que aplica el host, leída de la configuración en vivo en cada solicitud, por lo que tu edición se aplica sin reiniciar el plugin.

En esta versión, solo la concesión rige el acceso. El host verifica un destino contra egress_hosts y no exige además que aparezca en el manifiesto, por lo que una declaración ni concede un host ni limita una concesión que usted haya creado. La función de la declaración aquí es inicializar y comparar (diff), como se describe más abajo. Acotar el alcance efectivo a la intersección de declaración y concesión es trabajo de despliegue posterior, seguido en #8850.

Un egress_hosts vacío es el valor predeterminado y significa que no hay acceso a la red en absoluto: un permiso de transporte como http_client concede la superficie, y este campo concede los destinos. Las entradas son hosts exactos (api.example.com, 10.0.0.5) o patrones de sufijo explícitos (*.example.com, que coincide con los subdominios pero no con el dominio raíz, así que incluya el dominio raíz por separado cuando lo necesite). Los puertos no forman parte de una entrada: conceder un host concede todos los puertos de ese host, y no existe * con el significado de “en cualquier lugar”.

egress_hosts es deliberadamente un hermano en texto plano del mapa config cifrado, y no una clave dentro de él. La lista de permitidos es lo que un operador audita, así que se mantiene legible en el mismo archivo que se audita, mientras que los secretos junto a ella permanecen como enc2:….

¿Qué hacen install y list?

zeroclaw plugin install es el único momento en que ambos lados se reconcilian para el enlace de herramienta por defecto del paquete sin que tengas que escribir nada (la fila de una instancia de canal se crea al vincular su alias, con una decisión de concesión que indicas en la línea de comandos, como se describe en Binding a channel instance):

  • Para una fila que crea, inicializa la declaración en egress_hosts, muestra cada destino que concedió e imprime el comando zeroclaw config set que edita la concesión más tarde.
  • Para una fila que ya existe (una actualización, una reinstalación o una fila que usted creó), no cambia nada. En su lugar, muestra la diferencia: los destinos que el manifiesto declara y la fila no concede, los destinos que la fila concede y el manifiesto ya no declara, y el comando exacto que aplica la adición. Por lo tanto, una actualización del paquete no puede ampliar por sí misma su alcance de red; usted aplica la diferencia de forma deliberada.
  • Una reinstalación que encuentra una fila de nombre de paquete pre-1.0 no compatible rechaza la operación antes de crear una fila canónica e imprime los mismos pasos de actualización ordenados que plugin list. Al igual que plugin list, informa una vez, por sí solo, de un rechazo a nivel de despliegue (un security.nat64_prefixes mal formado o un plugins.limits.max_connections_per_instance con valor cero) y no imprime pasos de fila que no puedan surtir efecto hasta que se corrija ese problema. La instalación fallida se revierte antes de anunciar nada, y los valores anteriores de config, egress_hosts y egress_allow_private permanecen sin cambios hasta que actualices la configuración beta y vuelvas a intentarlo.

El comando impreso incluye la unión de la concesión existente y la declaración, porque zeroclaw config set reemplaza una lista en lugar de añadir elementos a ella. Ejecutarlo tal como se imprime añade los destinos declarados sin eliminar ningún host que hayas escrito tú mismo.

Todo comando impreso también comienza con zeroclaw --config-dir '<dir>', que indica el directorio de configuración con el que se calculó. --config-dir (y la variable ZEROCLAW_CONFIG_DIR que establece) solo afecta al proceso al que se lo pasas, así que un comando copiado de zeroclaw --config-dir /srv/a plugin list actuaría, de otro modo, sobre la configuración que tu shell resuelva por defecto. La clave de fila zpi1_… identifica el paquete, la capacidad y el enlace, pero no el perfil, así que ese comando reemplazaría la lista de permitidos de otro perfil con una lista calculada a partir de este. El directorio, la lista de hosts y cada otro argumento que lleva un comando (una ruta de configuración que nombra una propiedad del esquema, un paquete, un alias) se entrecomillan como un único argumento literal, de modo que nada de lo que declare un manifiesto puede ser expandido o sustituido por tu shell; pega el comando tal como se imprime. La excepción es la concesión para un paquete que no declara destinos, impresa como un comando config set --no-interactive para el egress_hosts de la fila sin valor: añade los hosts que usa tu despliegue como un único valor separado por comas. Ejecutado tal cual se imprime, se rechaza por falta de valor y no escribe nada. Un comando plugin bind impreso también está completo. Cuando el alias lo eliges tú, el comando lleva la palabra ALIAS en su lugar: tu shell pasa la palabra tal cual, y como un alias debe ir en minúsculas, el comando la rechaza como alias no válido y no escribe nada hasta que la sustituyas por el alias que elijas. Cuando crear la fila de la instancia requiere una decisión de salida de red, se imprimen dos comandos: primero uno que termina en --egress none y después uno que termina en --egress declared; ejecuta solo el que decidas. Pegar ambos a la vez deja la instancia sin alcance de red, porque el segundo comando encuentra la fila que creó el primero, y una fila existente nunca se amplía.

El entrecomillado sigue el shell de la plataforma en la que se imprimió el comando. En Linux y macOS se usa la forma de comillas simples de POSIX (sh, bash, zsh, fish), donde una comilla incrustada se escribe '\''. En Windows, ZeroClaw no puede saber si estás en cmd.exe o en PowerShell, por lo que imprime la única forma que ambos interpretan literalmente: cada valor entre comillas dobles, que es correcta para pegar en cualquiera de los dos shells siempre que el valor no contenga nada que cualquiera de los dos expanda dentro de comillas dobles. Una lista de hosts común y una ruta de perfil común, incluidos los espacios, siempre cumplen esta condición. Cuando un valor no la cumple (contiene ", %, !, $ o un backtick, o termina en una barra invertida; $(id).example.com es la forma que tendría un manifiesto malicioso), en su lugar se imprime toda la línea precedida por el marcador # PowerShell only, cmd.exe cannot pass this value literally:, seguido de un espacio y la forma de PowerShell, donde una comilla incrustada se duplica (''). Pegada completa, esa línea no ejecuta nada en ninguno de los dos shells (cmd.exe no puede ejecutar #, y PowerShell la interpreta como un comentario); copia el comando posterior al marcador solo en PowerShell. cmd.exe no tiene un entrecomillado que mantenga literal ese valor (%name% se expande dentro de sus comillas dobles, y una comilla simple es un carácter ordinario allí), por lo que nunca se confía en él para tales valores.

zeroclaw plugin list repite la misma comparación como diagnóstico permanente, una instancia a la vez: la vinculación de herramienta predeterminada de cada plugin de herramienta instalado que tenga http_client, y cada alias de canal vinculado cuyo paquete tenga http_client, websocket_client o socket_client, impresos como package (plugin.<alias>). Para cada una, una línea indica los destinos que declara y que su fila no concede, ya que las solicitudes a esos destinos se deniegan, junto con el comando que cierra la brecha; para una instancia que aún no tiene fila, ese comando crea la fila con la concesión. Una instancia de canal que aún no tiene fila, cuyo alias rechaza la gramática de alias de la configuración, como un [channels.plugin.ops-team] escrito a mano, recibe el motivo de la gramática y, en su lugar, la indicación de la tabla que debe renombrarse, ya que el comando que crea la fila valida la configuración completa y rechazaría ese alias. Una instancia de canal cuenta todos los transportes que la autoridad de egreso gobierna, porque un canal que habla solo por socket o WebSocket, como IRC o MQTT, de otro modo nunca se reportaría; una fila de herramienta conserva la regla de http_client con la que se distribuyó. La inversa nunca se marca. Una concesión sin declaración coincidente es una ruta de primera clase, no un hallazgo: el plugin cuyo destino es configuración de despliegue (un Gitea autoalojado, un Nextcloud en la LAN) no puede tener ese host declarado por su autor, así que tú creas la concesión.

Cuando la fila de la instancia todavía tiene una clave anterior a la 1.0 (un nombre de paquete en lugar de la clave zpi1_…), plugin list siempre muestra el cambio de nombre descrito arriba, porque el runtime resuelve una concesión únicamente por la clave zpi1_…: lo que concede esa fila no está en vigor, y las solicitudes se deniegan hasta que se renombre. Si la concesión de la fila ya cubre todo lo que declara el manifiesto, el cambio de nombre es la única instrucción: no se ofrece zeroclaw config set, ya que solo reemplazaría una lista que ya es correcta. Si la declaración todavía nombra destinos que la fila no concede, el cambio de nombre es el primer paso y el comando de concesión es el segundo, y ese comando conserva la concesión existente de la fila para no revocar nada de lo que usted configuró. Las rutas con puntos plugins.entries.<key>.… solo se resuelven en filas que ya están presentes en la configuración activa, así que ejecutar el comando de concesión antes del cambio de nombre falla con Unknown property; aplicar los pasos impresos en el orden indicado funciona. plugin list diagnostica esto y nunca edita la configuración por sí mismo. Solo la fila de una vinculación de herramienta predeterminada puede estar en este estado: las instancias de canal son posteriores al formato de clave anterior a la 1.0, por lo que no se informa ninguna fila de canal para un cambio de nombre.

plugin list evalúa una fila con el mismo constructor de políticas que usa el runtime en el momento de la solicitud, por lo que su veredicto es el del runtime. Si el runtime rechazaría la fila (un comodín de una sola etiqueta, como *.com, una entrada con espacios en blanco en los extremos, o una excepción de egress_allow_private que ningún host concedido cubre), el cargador de configuración solo emite una advertencia, pero el runtime rechaza toda la lista de permitidos y deniega cada solicitud. plugin list muestra el motivo del runtime y un comando de concesión construido solo con las entradas que el runtime acepta; en una fila pre-1.0, ese comando incluye el renombrado, y el renombrado nunca se ofrece por sí solo para una fila que el runtime rechazaría. El comando impreso reemplaza solo egress_hosts. Si una excepción privada seguiría siendo rechazada después, el informe lo indica y nombra la ruta egress_allow_private de la fila, ya que eso debe corregirse manualmente. Un rechazo que no tiene que ver con la fila en absoluto, como un security.nat64_prefixes mal formado o un plugins.limits.max_connections_per_instance igual a cero, rechaza por igual la política de todos los plugins; plugin list lo informa una sola vez, nombrando esas dos rutas, no imprime líneas por plugin hasta que se corrija, y nunca ofrece una reparación de concesión por plugin para ello. plugin install y plugin bind informan de una fila existente con el mismo veredicto, con las mismas palabras, de modo que ellos y plugin list nunca discrepen sobre si una concesión es utilizable.

A un paquete de canal sin alias vinculado todavía no tiene instancia, así que plugin list no muestra ninguna fila de canal para él: no hay fila con la que comparar, y la instalación no crea una.

Vinculación de una instancia de canal

Un paquete de canal no tiene instancia hasta que una tabla [channels.plugin.<alias>] lo nombre, y el alias lo eliges tú, así que instalar el paquete no puede crear por sí solo la fila de una instancia de canal. Vincular un alias es la ceremonia que sí lo hace:

zeroclaw plugin bind acme.chat --channel-alias operations --egress declared

plugin bind funciona sobre un paquete instalado; también sirve para añadir un segundo alias y para reparar un enlace parcial. zeroclaw plugin install <source> --channel-alias <alias> ejecuta el mismo procedimiento como parte de la instalación y acepta el mismo flag --egress, que install solo admite junto con --channel-alias. El flag solo afecta a la fila de la instancia del canal; una fila de herramienta del mismo paquete se genera según la regla propia de install, como siempre. En ese caso, todos los rechazos descritos a continuación se producen antes de publicar el paquete; la fila de herramienta, el enlace y la fila del canal se escriben en un único guardado; y si falla la escritura, el paquete se revierte. Una instalación de un paquete de canal sin --channel-alias lo publica como antes. Si ya hay enlaces que nombran el paquete, como los que deja un plugin remove, se imprime el informe de preparación de cada alias enlazado, descrito a continuación; si no, se imprime el comando plugin bind que hay que ejecutar, con ALIAS en lugar del alias que elijas. Cuando el manifiesto declara destinos a los que el canal puede llegar, se nombran y se imprime ese comando una vez por cada decisión de egreso: primero terminando en --egress none y después en --egress declared. El procedimiento nunca pide confirmación: cada decisión es un flag.

El comando se niega a ejecutarse y no escribe nada cuando:

  • El archivo de configuración usa una versión de esquema anterior; ejecute primero zeroclaw config migrate, ya que zeroclaw config set lo requiere, porque un guardado estamparía la versión actual sobre las secciones que todavía tienen la forma antigua;
  • el paquete no proporciona un canal;
  • El alias no tiene de 1 a 63 letras minúsculas, dígitos y guiones bajos simples, y debe comenzar y terminar con una letra o un dígito;
  • the alias ya está asociado a otro paquete, el que indica el mensaje de rechazo; el package de otro enlace nunca se reescribe;
  • el archivo de configuración, o su sección [plugins], [channels], [channels.plugin] o [channels.plugin.<alias>], está mal formado y no se cargó, porque el comando nunca sobrescribe una sección que el cargador descartó.

Si no, crea el enlace, escrito con package y el valor predeterminado enabled = true, o conserva uno que ya nombre el paquete exactamente como está, incluido enabled = false. Luego crea la fila [[plugins.entries]] de la instancia, cuando la fila todavía no existe y la instancia tiene estado de propiedad del host que almacenar: un config_schema, un destino declarado o un transporte que gobierna la autoridad de salida (http_client, websocket_client o socket_client).

Crear esa fila a partir de una declaración que la instancia puede usar requiere una decisión. Cuando el manifiesto declara destinos y el paquete contiene un transporte gobernado:

  • --egress declared siembra la declaración en egress_hosts e imprime cada destino junto con el comando que edita la concesión más adelante, la misma salida que muestra install para una fila de herramienta.
  • --egress none crea la fila con una concesión vacía, de modo que la instancia no tiene acceso a la red, e imprime los destinos declarados junto con el comando que los concede más adelante.
  • Sin ninguno de los dos flags, el comando se niega a ejecutarse, muestra los destinos declarados y menciona ambos flags. No se escribe nada.

Sin tal declaración no se necesita ninguna decisión: la fila se crea con una concesión vacía. Si el paquete contiene un transporte gobernado pero no declara destinos, el comando imprime un config set --no-interactive para el egress_hosts de la fila, sin valor, para que lo completes con los hosts que usa tu despliegue como un único valor separado por comas. Tal como se imprime, se rechaza y no escribe nada.

El flag solo se aplica a una fila que el comando crea. Una fila que ya existe, ya sea de una ejecución anterior, escrita por ti o dejada atrás por un paquete eliminado, nunca se amplía: el comando muestra la misma diferencia que install muestra para una fila existente y, si pasaste --egress, indica que no se aplicó. Por tanto, un script que siempre pasa --egress declared no puede ampliar una concesión. La fila de cada alias se inicializa solo a partir del manifiesto, nunca se copia de la fila de otro alias ni de la fila de herramienta del paquete, y egress_allow_private nunca se escribe. Una segunda ejecución con los mismos argumentos deja el archivo de configuración byte a byte idéntico, y una ejecución que encuentra un enlace sin su fila, o una fila sin su enlace, crea la mitad que falta sin tocar la otra.

Después de la vinculación, el comando informa si la instancia puede iniciarse, leyendo cada respuesta desde la fuente que usa el runtime:

  1. El conjunto de permisos que tiene la instancia, es decir, los que solicita su manifiesto. Se muestran aquí, no se deciden: la concesión que decides es la lista de permitidos de salida (egress allowlist).
  2. Cada clave que el esquema enumera como required, establecida o ausente. Una clave ausente incluye el comando config set plugins.entries.<key>.config.<name> que la establece, y el comando de una clave secreta solicita el valor sin mostrarlo en pantalla. No se imprime ningún comando para un nombre de propiedad fuera de la gramática de claves portable; el informe indica en su lugar la fila que debe editarse en el archivo de configuración. Una instancia cuya fila aún no existe, como un enlace escrito a mano, recibe el comando plugin bind que crea la fila en su lugar, en lugar de comandos por clave, ya que config set solo resuelve filas existentes. Cuando el manifiesto declara destinos que la fila puede usar, el informe los nombra e imprime ese comando una vez por cada decisión de egreso, primero terminando en --egress none y luego en --egress declared, para que ejecutes el que elijas. Un enlace escrito a mano cuyo alias plugin bind rechaza, como [channels.plugin.ops-team], recibe el motivo de la gramática de alias y la tabla que hay que renombrar. Los valores nunca se imprimen.
  3. El veredicto del resolvedor de configuración en tiempo de ejecución sobre la fila, que indica la ruta del esquema o la propiedad que falla cuando el resolvedor informa de una, y nunca un valor. Esto incluye una fila que plugin remove conservó y que aún contiene valores cuando el manifiesto reinstalado no declara config_schema: el runtime rechaza cualquier valor allí, y el informe identifica la fila por su clave, con la solución que corresponde. Cuando la instancia no posee ningún otro estado, elimine la fila. Cuando la fila todavía conserva la concesión de salida (egress) de la instancia, porque el manifiesto declara destinos o solicita un transporte gobernado, elimine solo los valores bajo su mapa config y conserve la fila.
  4. Para una instancia cuya fila existe, la brecha de egreso, con las mismas palabras que usa plugin list. A una instancia que aún no tiene fila no se le muestra aquí ninguna línea de brecha: los comandos plugin bind del punto 2 crean la fila con tu decisión de egreso, mientras que plugin list muestra una reparación config patch que crea la fila ya concedida. plugin bind y plugin install --channel-alias omiten la brecha, porque acaban de informar de la concesión de la fila junto con su comando.
  5. El veredicto propio del plan de activación, que indica la primera condición previa que el runtime detecta como no cumplida. Comprueba, en este orden, plugins.enabled, el enabled del enlace, un agente habilitado que incluya plugin.<alias> en su channels, que el paquete esté instalado y proporcione un canal, y que haya una ranura libre bajo plugins.max_active_instances. Un sistema de plugins deshabilitado va acompañado del comando que lo habilita, y lo mismo ocurre con un enlace deshabilitado cuyo alias acepta la gramática de alias de configuración. No se muestra ningún comando para cualquier otro alias, como un [channels.plugin."a.b"] escrito a mano: el informe indica la tabla en la que establecer enabled = true manualmente, y en el caso de un alias con puntos como ese, config set crearía otro enlace. Un propietario ausente se describe en lugar de ofrecer un comando, porque config set sobre la lista channels de un agente reemplaza la lista completa. Cuando se cumplen todas las condiciones previas, el plan admite la instancia y esta se inicia en el próximo arranque del daemon o recarga, si su componente se carga. Si el runtime no puede construir su plan de activación, el informe lo indica junto con el error del runtime.
  6. Un daemon en ejecución solo inicia la instancia después de un reinicio o una recarga.

El ceremonio nunca activa plugins.enabled ni edita los channels de un agente: ambas decisiones siguen siendo tuyas. Una vinculación que se completa correctamente sale con código cero incluso cuando la instancia todavía no puede arrancar; solo una negativa o una escritura fallida sale con código distinto de cero. zeroclaw plugin info <package> muestra el mismo informe después de la clave de cada alias vinculado y, para un paquete de canal sin alias vinculado, muestra la misma sugerencia de plugin bind que muestra la instalación.

Un enlace cuyo alias no puede ser referenciado por ninguna instancia de plugin, como uno que contiene un carácter de control (la validación de la configuración solo emite una advertencia), no impide que se informe de las demás instancias del paquete. plugin list, plugin info, plugin install y plugin remove muestran una línea que indica que el enlace se omite, con el alias escapado, y nombran la tabla [channels.plugin.<alias>] que hay que renombrar o eliminar.

Instalación desde la guía de inicio rápido

Una compilación con el host de plugins también puede instalar plugins de herramientas desde la fila opcional Plugins de la guía de Inicio rápido de la CLI; Inicio rápido → Plugins explica el procedimiento. El Inicio rápido ejecuta la misma transacción de descarga, admisión, comprobación de carga y publicación y siembra que zeroclaw plugin install, y las reglas anteriores siguen vigentes:

  • Declaración frente a concesión. Quickstart muestra la declaración [egress] del manifiesto, que por sí sola no concede nada. Cada vez que crea una fila para un paquete cuyo manifiesto solicita http_client y declara destinos, la única declaración con la que se inicializa una fila creada, decides si la nueva fila concede esos destinos, tanto si el paquete es nuevo como si se instaló antes sin su fila. La pregunta aparece al instalar sin acceso a la red, lo que deja egress_hosts vacío e imprime el comando que concede los destinos más adelante, como hace plugin bind --egress none con una instancia de canal. Omitir un paquete ya instalado deja su fila ausente e imprime el comando que la crea con una concesión vacía; a continuación, en una línea propia, el comando que concede los destinos declarados. El comando que Quickstart imprime para crear una fila que falta nunca concede acceso a la red; una concesión es siempre un comando aparte. El comando plugin install --no-verify que Quickstart imprime para un plugin que no supera la comprobación de carga es la instalación de la CLI de plugins, que, como siempre, inicializa una fila que crea con los destinos que el manifiesto declara para http_client.
  • Una fila existente nunca se amplía. Cuando la fila [[plugins.entries]] de la instancia ya existe, Quickstart deja egress_hosts, egress_allow_private y su configuración exactamente como están. Nunca descarga, actualiza ni reconfigura un paquete que ya está instalado, y solo crea la fila de ese paquete si falta.
  • La activación requiere tu consentimiento. Quickstart nunca activa plugins.enabled ni plugins.auto_discover sin preguntar. Antes de preguntar, enumera cada plugin de herramientas, skills y canales que esas configuraciones activarían, incluidos los plugins instalados anteriormente, y la pregunta tiene por defecto la respuesta no cuando esa lista excede los plugins que la ejecución instaló o conservó. Un plugin cuya fila faltante omitiste, o que Quickstart no pudo terminar de configurar, no forma parte de esa lista. La lista se obtiene del directorio de plugins tal como lo cargaría un daemon; cuando ese directorio no puede leerse, la pregunta también tiene por defecto la respuesta no.

Quickstart es más estricto que plugin install en tres aspectos. Rechaza una entrada del registro que no incluye un digest sha256 para su archivo, en lugar de instalar un archivo que no puede verificar. Mientras no se pueda leer la sección [plugins] del archivo de configuración, no instala nada, mientras que plugin install instala el paquete y solo advierte que no creó la fila [[plugins.entries]] del paquete. Y, sea cual sea el valor de plugins.security.signature_mode, su resumen del paquete indica si la firma del manifiesto se verifica con plugins.security.trusted_publisher_keys, y solo instala un paquete cuya firma no se verifica si respondes sí a una pregunta cuya opción por defecto es no; signature_mode sigue determinando lo que rechaza la admisión.

Actualizando un plugin

zeroclaw plugin update reemplaza un paquete instalado por otra versión del mismo paquete y conserva todo lo que pertenece a sus instancias:

# la versión que selecciona el registro para cada plugin con nombre
zeroclaw plugin update my-plugin other-plugin

# una versión exacta, más reciente o más antigua que la instalada
zeroclaw plugin update my-plugin@0.2.0

# cada plugin instalado
zeroclaw plugin update --all

# un directorio de paquete local, por ejemplo un plugin recompilado
zeroclaw plugin update my-plugin --from ./my-plugin/

--registry <url> selecciona el registro igual que lo hace con plugin install. Update solo reemplaza paquetes que están instalados: un nombre desconocido falla con una indicación de usar plugin install, y plugin install sobre un paquete ya instalado falla con una indicación de usar plugin update. Los paquetes se identifican solo por nombre, por lo que --all consulta al registro por cada paquete instalado, incluido uno que hayas instalado desde un directorio; nombra los plugins explícitamente cuando ejecutes compilaciones locales. El comando update forma parte del ciclo de vida del registro seguido en #7432 (R3).

Selección de versión. Un nombre sin versión toma la versión que elegiría plugin install, es decir, la última entrada que el registro lista para ese nombre, y name@version toma exactamente esa entrada. ZeroClaw no ordena las versiones para elegir una, así que una versión seleccionada distinta de la instalada se instala aunque sea más antigua. Cuando ambas son versiones semánticas y la nueva es anterior, la línea de resultado lo indica en lugar de informar de una actualización. Fijar la versión que tenías es la forma de volver atrás. Cuando la versión seleccionada es la instalada, el plugin se informa como actualizado y no se descarga nada. Un paquete --from siempre reemplaza al instalado, incluso con la misma versión, de modo que un plugin recompilado puede reinstalarse sin subir la versión.

Verificación. Un reemplazo supera las mismas comprobaciones que realiza una instalación nueva antes de que cambien el paquete instalado o su configuración: el digest del archivo cuando la entrada del registro lo indica, el nombre y la versión del manifiesto frente a la entrada del registro, la política de firmas, el digest del payload, el esquema de configuración y la comprobación de carga contra el mundo WIT de este host con tus plugins.limits. La actualización no admite --no-verify. Para instalar un paquete que no carga, elimina el plugin e instálalo con --no-verify.

Autoridad añadida. El host concede a un plugin todos los permisos que solicita su manifiesto. Por eso, una actualización se rechaza antes de compilar su componente cuando el nuevo manifiesto solicita un permiso o una capacidad que la versión instalada no solicitaba, declara un nuevo canal provides, o indica una clave de editor distinta a la de la versión instalada (o ninguna, si la versión instalada sí indicaba una). La clave identifica a un editor solo cuando plugins.security.signature_mode es strict, modo en el que se verifica; en los modos permissive y disabled es una afirmación no verificada, y una clave que aparece donde la versión instalada no indicaba ninguna no se trata como un cambio. El rechazo enumera lo que añade la nueva versión y el comando que acepta exactamente eso, fijado a la versión que revisaste:

'my-plugin' 0.2.0 requests authority the installed version does not have:
  + permission:http_client
Nothing was changed. To accept it and update, run: zeroclaw --config-dir '/home/user/.zeroclaw' plugin update 'my-plugin@0.2.0' --allow-token 'permission:http_client'

--allow-token acepta un elemento completo por marca y puede repetirse: permission:<name>, capability:<name>, provides:<id> o publisher:<key> (publisher:unsigned para un paquete que no indica ninguna clave). --allow acepta los mismos elementos en una única lista separada por comas. El comando impreso usa --allow-token, así que un elemento cuyo valor contiene una coma, como un id de provides, llega al comando completo. Cualquiera de las dos marcas se aplica a un plugin a la vez, y para una actualización del registro ese plugin debe fijarse como name@version, de modo que lo que aceptas es la versión que revisaste. La otra forma de aceptarlo es eliminar el plugin e instalar la nueva versión. Todos los valores del comando impreso están entrecomillados para tu shell, siguiendo las reglas descritas en Declaring and granting egress.

Qué conserva una actualización. La identidad de una instancia es el nombre del paquete, la capacidad y el enlace, no la versión; por tanto, las filas [[plugins.entries]] del plugin, sus valores de configuración y secretos, egress_hosts, egress_allow_private, los perfiles TLS y el estado persistente se mantienen sin cambios. La regla de egreso de plugin install se aplica: una actualización nunca amplía un permiso, ni siquiera en una fila que crea para una instancia que no tenía ninguno. Imprime la diferencia entre la nueva declaración y el permiso vigente, junto con el comando que la aplica. Cuando el esquema de configuración de la nueva versión rechaza una configuración que una instancia ya tiene, por ejemplo un ajuste recién obligatorio, la actualización se completa igualmente y nombra la instancia y el problema. Esa instancia no se carga hasta que su configuración se ajuste a la nueva versión. Un paquete cuya fila de configuración todavía usa la clave anterior a 1.0 (con el nombre del paquete) no se actualiza hasta que esa fila se renombre, y el rechazo muestra la misma guía que plugin install.

Resultados. Cada plugin obtiene un resultado: actualizado, ya está actualizado, omitido (con --all, un paquete que el registro no lista, o uno que una actualización interrumpida dejó desplazado, lo que se indica como desplazado), requiere aprobación, o no actualizado, con el motivo y la versión que sigue instalada. Que un plugin falle no detiene a los demás. El comando termina con un código distinto de cero cuando algún plugin no se actualizó o requiere aprobación, de modo que un script puede tomar decisiones según ese resultado.

Reemplazo y recuperación. Una actualización solo cambia el directorio de plugins mientras mantiene el mismo bloqueo oculto que plugin install y plugin remove, de modo que esos cambios nunca se superponen, ni siquiera entre procesos. El nuevo paquete se verifica primero y luego se escribe completo en una transacción de preparación oculta, tal como lo hace install. Solo entonces la actualización reclama el directorio instalado en una transacción oculta propia y comprueba que sigue siendo el paquete con el que se preparó la actualización, con el mismo manifiesto y componente. Después mueve el nuevo paquete a su lugar sin sobrescribir nada, y si ese movimiento falla, devuelve el paquete anterior a su lugar. Un fallo antes de la reclamación deja el paquete instalado como estaba, y un paquete que cambió en disco desde que empezó la actualización se devuelve a su lugar y la actualización se rechaza. Si el proceso se detiene después de la reclamación y antes de que el nuevo paquete esté en su lugar, el paquete anterior sobrevive en esa transacción oculta, que el descubrimiento ignora, y el bloqueo del sistema operativo sobre él termina con el proceso. plugin list entonces indica que el paquete está desplazado, tras esperar a cualquier instalación, actualización o recuperación en curso, y la siguiente zeroclaw plugin update <name> lo devuelve a su lugar antes de hacer cualquier otra cosa, incluido antes de consultar el registro, de modo que la recuperación funciona sin conexión. La actualización continúa después con normalidad, así que para un paquete que instalaste desde un directorio, añade --from con ese directorio; de lo contrario, el paquete del registro con ese nombre podría reemplazarlo. --all solo apunta a ese comando, así que un paquete desplazado nunca se devuelve a menos que lo indiques por nombre, ni siquiera uno que una actualización desplazó mientras se ejecutaba el lote. Una versión reemplazada se marca para eliminación dentro de su transacción antes de borrarse, así que una que no pueda borrarse nunca se devuelve: la advertencia indica dónde está, y la siguiente actualización del plugin la elimina. Si incluso el marcado falla, borra a mano el directorio que indica la advertencia, porque una vez eliminado el plugin, zeroclaw plugin update <name> volvería a poner esa copia. Un proceso detenido después de que el nuevo paquete esté en su lugar pero antes de que la versión reemplazada esté marcada la deja del mismo modo: la siguiente actualización del plugin la elimina, pero tras un plugin remove, plugin list la indica como desplazada y una actualización con nombre la devuelve a su lugar.

Los archivos no se sincronizan en disco antes de los renombrados, como ocurre con plugin install, por lo que una pérdida de energía justo después de una actualización puede dejar el nuevo paquete incompleto. Volver a actualizar no lo repara, porque el paquete dañado ya tiene la versión que selecciona el registro. Si zeroclaw plugin info <name> indica que el plugin no se carga, ejecuta zeroclaw plugin remove <name> y vuelve a instalarlo; sus filas de configuración y permisos concedidos se conservan tras la eliminación. Si el plugin ya no aparece en la lista, elimina a mano su directorio dentro de plugins_dir y luego vuelve a instalarlo.

Daemon en ejecución. La actualización solo cambia el paquete instalado y nunca envía señales a un daemon en ejecución. Los conjuntos de herramientas que el daemon construye después de la actualización cargan la nueva versión, mientras que las instancias de canal conservan el componente que cargaron hasta que el daemon se reinicie o recargue sus canales; por eso, reinícialo para mover todas las instancias a la nueva versión. Un conjunto de herramientas construido en el instante exacto del cambio puede omitir el plugin o, en el caso de un paquete cuyo manifiesto no vincula wasm_sha256, combinar el manifiesto anterior con el nuevo componente; el siguiente conjunto de herramientas será coherente.

Dónde está realmente el límite de confianza

El sandbox limita lo que puede hacer un plugin cargado; la política de firmas limita lo que se carga en absoluto. Ambas son decisiones del operador, y se combinan:

  • plugins.enabled false (el valor predeterminado): no se ejecuta ningún código de plugin, nunca.
  • plugins.auto_discover false (el valor predeterminado): las capacidades de herramientas y habilidades detectadas automáticamente no se cargan. plugins.enabled = true por sí solo activa únicamente los canales que declares en [channels.plugin.<alias>]; las herramientas y habilidades se cargan solo cuando auto_discover = true también está establecido.
  • Firma strict: solo los componentes cuyo manifiesto lleve una firma Ed25519 válida de una clave de tu conjunto de confianza se cargan.
  • Plugin cargado: limitado por combustible, topes de memoria, WASI sin preapertura y el conjunto de importaciones con permisos controlados.

Lo que el sandbox no delimita es el comportamiento semántico de una herramienta que el modelo decide invocar: una herramienta con la concesión http_client y la superficie HTTP del adaptador de la herramienta puede enviar lo que el modelo le pase a donde su código decida. La política de firmas existe porque “qué código cargo” es la decisión que más importa; tómala de forma deliberada.

Confianza de certificados para el complemento HTTPS

Una solicitud de plugin sobre HTTPS verifica el certificado con el programa de raíces webpki incluido y con las raíces que esta máquina ya confía. Por lo tanto, un endpoint cuyo certificado encadena a una CA instalada localmente, como una raíz de MDM empresarial o una PKI privada, funciona para los plugins exactamente igual que ya funciona para las solicitudes al proveedor. La verificación en sí no cambia: la construcción de la cadena y la coincidencia del nombre de host siguen vigentes, y la política de egreso sigue decidiendo a qué destinos puede acceder un invitado.

Los sockets y las conexiones WebSocket parten de las mismas raíces. Un plugin que necesite acceder a un servicio detrás de una CA privada, o presentar un certificado de cliente, indica un perfil TLS que el operador configuró en su instancia:

[[plugins.entries]]
name = "zpi1_…"                       # la clave de la instancia
egress_hosts = ["imap.corp.example.com"]

[[plugins.entries.tls_profiles]]
name = "corp"
hosts = ["imap.corp.example.com"]     # debe estar dentro de egress_hosts
system_roots = false                  # confiar solo en la CA indicada abajo
custom_ca_secret = "corp_ca"          # propiedades x-secret del esquema de configuración del plugin
client_certificate_secret = "cert"    # que contienen material PEM
client_private_key_secret = "key"

Un perfil selecciona solo certificados. Cada uno de sus hosts debe estar autorizado por egress_hosts, lo que comprueba la validación de la configuración, y una solicitud que lo nombre sigue pasando primero por la concesión ordinaria. El material de los certificados permanece en la configuración cifrada de la instancia; los campos del perfil son solo los nombres de las propiedades. El host lee ese material al construir una conexión, y el plugin no puede: secrets.get rechaza cualquier propiedad que un perfil nombre, por lo que una clave privada de cliente nunca entra en el invitado.

Esas raíces se leen una sola vez por proceso. Reescribir el archivo de certificados en la misma ruta, o cambiar el almacén del sistema operativo, no llega a un daemon en ejecución; reinícialo antes de esperar que el HTTPS de los plugins vea el cambio. Lo mismo aplica a una primera lectura desafortunada: una máquina cuyo almacén estuvo brevemente ilegible sirve solo la confianza incluida en el binario hasta que el proceso se reinicia, y la línea de registro plugin_egress_trust_anchors es la que indica cuál de los dos casos ocurrió.