Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Escribir un complemento de herramienta

Esta es la guía de nivel inicial de la serie: un recorrido completo y práctico desde una crate vacía hasta una herramienta que el modelo llama en conversación. La herramienta creada aquí es redact, que enmascara correos electrónicos, prefijos de credenciales conocidos y patrones proporcionados por el operador en texto. Está deliberadamente guiada por configuración, porque leer tu propia sección de configuración aislada es lo que necesita todo plugin no trivial y lo más fácil de hacer mal.

Todo en esta página se verifica contra el código fuente del contrato: el world tool-plugin en wit/v0/tool.wit, la ruta de llamada del lado del host en crates/zeroclaw-plugins/src/runtime.rs y wasm_tool.rs, y la validación del manifiesto en host.rs. Las rutas de origen son citas dentro del repositorio de ZeroClaw para su verificación; el plugin en sí es tu propia crate en tu propio repositorio. Nunca necesitas un checkout de ZeroClaw para compilar uno, solo los archivos de contrato wit/ (obtenidos en el paso 1) y un binario zeroclaw instalado con el host de plugins compilado para ejecutarlo.

El binario de la versión no es ese binario. Los binarios precompilados que incluye el instalador no incluyen el host de plugins (zeroclaw plugin … es un subcomando no reconocido), y plugins-wasm no está en el conjunto de características predeterminado del crate. Compila el lado del host desde el código fuente con un backend de ejecución; cada característica de backend incluye por sí misma el paraguas plugins-wasm, así que una sola marca es suficiente:

cargo build --release --features plugins-wasm-cranelift

La página del protocolo documenta las opciones del backend.

Cómo fluye una llamada a una herramienta

Comprende la forma de ejecución antes de escribir código:

  1. Al iniciarse, el descubrimiento encuentra el directorio del complemento, valida la estructura del manifiesto, aplica la política de firmas y, a continuación, valida config_schema. Antes del registro, el host materializa los valores de operador del complemento como JSON con tipos y los valida. Los que superan la validación se convierten en instancias de WasmTool.
  2. Durante el registro, el host instancia el componente una vez para leer name, description y parameters-schema. Estos datos se almacenan en caché; nunca se vuelven a solicitar. Si ese sondeo falla, el registro falla; el host nunca sustituye los metadatos sintéticos por los de un componente defectuoso.
  3. Por llamada, WasmTool::execute resuelve y valida la configuración a partir del estado canónico, crea un almacén nuevo (contexto WASI nuevo, nuevo presupuesto de combustible y ningún estado de la llamada anterior) e instancia el componente. Ese único objeto resuelto sirve para todo el marco: el host inyecta únicamente sus valores no secretos bajo __config, proporciona los secretos marcados por el esquema mediante la importación secrets con ámbito restringido e invoca execute.

El modelo de tienda nueva por llamada es la restricción de diseño que más importa: un complemento de herramienta no tiene estado por construcción. Todo lo que quieras conservar entre llamadas tiene que vivir fuera del complemento (en el texto que devuelves, o en la configuración del operador).

1. Configuración del crate

Crea el crate y añade las dependencias del lado del guest:

cargo new --lib my-plugin
cd my-plugin
cargo add wit-bindgen@0.46
cargo add serde --features derive
cargo add serde_json

Luego, haga dos ediciones manuales en el manifiesto del paquete:

  1. Establece crate-type de la biblioteca en ["cdylib", "rlib"]. cdylib es lo que produce la compilación del componente; rlib permite que los módulos de lógica pura del mismo crate se compilen y se prueben unitariamente de forma nativa en el host.
  2. En el perfil de lanzamiento, establece opt-level = "s", lto = true y strip = true. El tamaño del componente influye en el tiempo de descarga y de carga; no hay razón para enviar símbolos de depuración a través del límite del plugin.

Copia el directorio wit/v0/ del repositorio de ZeroClaw en la raíz del crate como wit/. No necesitas un checkout completo; obtén solo ese directorio desde la etiqueta que coincida con la versión de tu host de destino:

git clone --depth 1 --filter=blob:none --sparse \
  https://github.com/zeroclaw-labs/zeroclaw /tmp/zeroclaw-wit
git -C /tmp/zeroclaw-wit sparse-checkout set wit
cp -r /tmp/zeroclaw-wit/wit .

Los archivos WIT son la ABI: el host generó sus bindings a partir de estos archivos exactos, así que tus bindings de guest deben provenir de los mismos. Fija la versión: los worlds de WIT evolucionan con el host, y un componente compilado contra worlds más nuevos que los que vincula el host fallará al instanciarse.

2. Separar la lógica del código de unión

Ponga el comportamiento real en un módulo Rust simple sin imports de wit-bindgen, y mantenga delgada la capa de pegamento del componente. La razón es la facilidad de prueba: el target de componente no puede ejecutar cargo test de forma nativa, así que la lógica atrapada en el pegamento es lógica que solo puede verificarse de extremo a extremo a través de un host wasm. El pegamento debe ser demasiado delgado para estar mal.

src/redact.rs contiene una estructura de configuración y una función pura:

#![allow(unused)]
fn main() {
pub const DEFAULT_REPLACEMENT: &str = "[REDACTED]";

/// Política de redacción resuelta a partir de la propia sección de configuración del plugin.
#[derive(Debug, serde::Deserialize)]
#[serde(default, deny_unknown_fields)]
pub struct RedactConfig {
    pub replacement: String,
    pub redact_emails: bool,
    pub patterns: Vec<String>,
}

impl Default for RedactConfig {
    fn default() -> Self {
        Self {
            replacement: DEFAULT_REPLACEMENT.to_string(),
            redact_emails: true,
            patterns: Vec::new(),
        }
    }
}

/// Redactar la entrada. Devuelve la salida y el número de intervalos enmascarados.
pub fn redact(input: &str, cfg: &RedactConfig) -> (String, usize) {
    // Enmascara correos electrónicos cuando cfg.redact_emails, prefijos de credenciales
    // (sk-, ghp_, AKIA, xoxb-), y cada literal en cfg.patterns,
    // reemplazando cada coincidencia con cfg.replacement.
    // ...
}
}

El huésped recibe el objeto JSON público materializado por el esquema, así que debe deserializarlo una sola vez en lugar de repetir el análisis de cadenas. El esquema de este ejemplo hace que todos los campos sean opcionales, y Default define su comportamiento cuando el host proporciona {}. Un objeto vacío es normal cuando el operador no ha configurado el complemento o cuando el host deniega la concesión config_read solicitada. Si un complemento no puede funcionar sin un valor, marque ese campo como obligatorio en config_schema; el host rechazará entonces un objeto vacío antes de que se inicie el código del huésped.

3. Implementa el mundo

wit/v0/tool.wit define la superficie que debes exportar. El world es:

world tool-plugin {
    import logging;
    import secrets;
    export plugin-info;
    export tool;
}

y la interfaz tool consta de cuatro funciones:

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>;

src/lib.rs genera los enlaces del invitado e implementa ambas exportaciones:

#![allow(unused)]
fn main() {
pub mod redact;

#[cfg(target_family = "wasm")]
mod component {
    wit_bindgen::generate!({
        path: "wit/v0",
        world: "tool-plugin",
        features: [plugins-wit-v0],
    });

    use crate::redact::{redact, RedactConfig};
    use exports::zeroclaw::plugin::plugin_info::Guest as PluginInfo;
    use exports::zeroclaw::plugin::tool::{Guest as Tool, ToolResult};
    use zeroclaw::plugin::logging::{
        log_record, LogLevel, PluginAction, PluginEvent, PluginOutcome,
    };

    struct RedactPlugin;

    #[derive(serde::Deserialize)]
    struct ExecuteArgs {
        text: String,
        #[serde(rename = "__config", default)]
        config: RedactConfig,
    }

    impl PluginInfo for RedactPlugin {
        fn plugin_name() -> String {
            "my-redact-plugin".to_string()
        }
        fn plugin_version() -> String {
            "0.1.0".to_string()
        }
    }

    impl Tool for RedactPlugin {
        fn name() -> String {
            redact.to_string()
        }

        fn description() -> String {
            "Redacta secretos y PII del texto antes de que llegue a un registro, \
             canal o modelo. Oculta correos electrónicos, prefijos de credenciales y \
             patrones literales configurados por el operador."
                .to_string()
        }

        fn parameters_schema() -> String {
            serde_json::json!({
                "tipo": "objeto",
                "propiedades": {
                    "texto": {
                        "tipo": "cadena",
                        "descripción": "El texto para redactar."
                    }
                },
                "requerido": ["texto"]
            })
            .to_string()
        }

        fn execute(args: String) -> Result<ToolResult, String> {
            let parsed: ExecuteArgs = match serde_json::from_str(&args) {
                Ok(a) => a,
                Err(e) => {
                    return Ok(ToolResult {
                        success: false,
                        output: String::new(),
                        error: Some(format!("argumentos no válidos: {e}")),
                    });
                }
            };

            let (output, count) = redact(&parsed.text, &parsed.config);

            log_record(
                LogLevel::Info,
                &PluginEvent {
                    function_name: "my_redact_plugin::tool::execute".into(),
                    action: PluginAction::Complete,
                    outcome: Some(PluginOutcome::Success),
                    duration_ms: None,
                    attrs: Some(format!("{{\"redactions\":{count}}}")),
                    message: "redacted input".into(),
                },
            );

            Ok(ToolResult { success: true, output, error: None })
        }
    }

    export!(RedactPlugin);
}
}

Puntos de contrato, cada uno anclado en la fuente del host:

  • plugin-info es una exportación obligatoria de cada world. Indica el nombre y la versión del propio componente. Mantén ambos sincronizados con el manifiesto.
  • Los metadatos se leen una sola vez. call_tool_metadata en runtime.rs lee name, description y parameters-schema en el registro y los almacena en caché. No los calcules a partir de nada dinámico; nunca se volverán a observar.
  • El esquema es la vista completa del modelo de tu herramienta. El host lo analiza como JSON al cargarlo (tool parameters-schema is not valid JSON es un fallo duro de registro) y lo reenvía al LLM tal cual. Describe cada propiedad. Nunca declares __config en él: esa clave está reservada por el host, y el host elimina cualquier valor suministrado por el llamante antes de la inyección precisamente para que el modelo no pueda hacerse pasar por tu operador.
  • success: false frente a Err. Un ToolResult con success: false vuelve al modelo como una respuesta normal de la herramienta a la que puede reaccionar (reintentar con argumentos corregidos, disculparse, elegir otra herramienta). Un Err(String) cruza el límite como un fallo del plugin: el host lo envuelve como plugin execute returned error y la llamada falla. Reserva Err para estados realmente rotos e informa de entradas incorrectas mediante success: false.
  • Registra a través de la interfaz logging importada, nunca wasi:logging. log-record es de enviar y olvidar; el host absorbe todos los errores, así que una escritura de registro fallida nunca puede hacer fallar tu llamada, y los eventos llegan a cada destino en el que escribe zeroclaw_log, llevando la atribución zeroclaw.* (agent_alias, session_key, proveedor, canal) del span del host bajo el que se ejecuta tu llamada. Ten en cuenta que el campo attrs en plugin-event no es atribución: es la carga útil attributes de forma libre de la fila de registro. La atribución está vinculada al alias, se hereda del span de trazas ambiente en el lado del host, y nada de lo que envíe un plugin puede establecerla ni sobrescribirla. PluginAction y PluginOutcome son enums cerrados que reflejan las taxonomías del host; a propósito no hay ninguna variante de forma libre. Elige la más cercana.

4. La jaula __config

Un plugin nunca lee las variables de entorno del proceso ni ve la configuración global. Un manifiesto que solicita config_read también debe declarar config_schema; un esquema sin ese permiso también es inválido. El esquema es Draft 2020-12, su raíz debe ser un objeto con un mapa properties y additionalProperties = false, y cada propiedad de nivel superior debe resolverse explícitamente como string, boolean, integer, number, array u object.

El host resuelve la sección almacenada bajo la clave de entrada de configuración versionada derivada del paquete de esta instancia, la capacidad tool y la vinculación, la materializa conforme al esquema del paquete, valida el objeto tipado completo y solo entonces lo particiona. Solo las propiedades que no son secretas se combinan en execute bajo la clave reservada __config:

  • Cualquier __config ya presente en los argumentos proporcionados por el modelo se elimina primero. La suplantación es estructuralmente imposible.
  • El almacenamiento del operador sigue siendo un mapa de cadenas cifrado. Almacena las cadenas directamente; codifica los valores booleanos y numéricos como escalares JSON ("true", "4", "0.5") y las matrices y los objetos como JSON ('["secret-a","secret-b"]'). El invitado recibe valores booleanos, números, matrices y objetos JSON reales, no esas cadenas de almacenamiento.
  • Una propiedad de tipo cadena directa de nivel superior marcada con x-secret = true se excluye de __config. Léala explícitamente con la función generada zeroclaw::plugin::secrets::get. Los marcadores anidados, los marcadores falsos o no booleanos y las propiedades secretas que no sean cadenas impiden la admisión del manifiesto.
  • El host habilita las lecturas de secretos solo al despachar execute. Las llamadas desde la inicialización del componente o las exportaciones de metadatos devuelven unavailable sin resolver la configuración. El __config público y las lecturas de secretos durante una ejecución usan la misma vista de configuración resuelta.
  • Si se solicitó config_read pero no se concedió de forma efectiva, el host resuelve {} y lo valida. Por lo tanto, el esquema opcional de este ejemplo hace que la herramienta omita __config, y #[serde(default)] selecciona RedactConfig::default. Un esquema obligatorio falla de forma segura en lugar de ejecutarse sin credenciales.
  • Las claves desconocidas, las codificaciones JSON no válidas, los tipos incorrectos y los fallos de las restricciones del esquema hacen que el complemento se rechace antes de que se ejecute su código. Actualmente, los operadores establecen los valores bajo la clave de instancia que muestra la instalación mediante TOML o la ruta genérica zeroclaw config set; esos valores se cifran en reposo con la clave secreta de la configuración. Los editores de zerocode y de la pasarela basados en esquemas son trabajo futuro del SDK y de la superficie de configuración.

Para esta herramienta, la sección tipada tiene tres claves opcionales: replacement es una cadena, redact_emails es un valor booleano y patterns es una matriz de cadenas.

5. El 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:

CampoObligatorioSignificado
nameSometimes 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.
versionSometimes 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.
descriptionnoDescripción legible para humanos mostrada por zeroclaw plugin list.
authornoNombre del autor u organización.
wasm_pathpara las capacidades de WASMNombre 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.
capabilitiessí, no vacíoQué es el plugin: cualquiera de tool, channel, memory, observer, skill (PluginCapability, serializado en snake_case).
permissionsnoServicios del host a los que puede acceder el código: http_client, config_read, file_read, file_write, memory_read, memory_write (PluginPermission). Actualmente solo se aplican los dos primeros; el resto se acepta, pero no tiene efecto. Declarar config_read requiere config_schema, y actualmente solo los adaptadores de herramientas/canales lo proporcionan.
config_schemaexactamente con config_readBorrador 2020-12 de JSON Schema para la configuración privada de este complemento; el esquema se incluye en los bytes del manifiesto canónico y, por tanto, queda cubierto cuando se firma el manifiesto. La raíz debe ser un objeto con un mapa de properties y additionalProperties = false. Cada propiedad de nivel superior debe tener un único tipo compatible explícito, directamente o mediante un puntero JSON local: string, boolean, integer, number, array u object. Los consumidores de herramientas y canales pueden establecer x-secret = true directamente en una propiedad de cadena de nivel superior para eliminarla de la configuración pública y exponerla mediante la importación del host con ámbito secrets.get. Las herramientas reciben la configuración pública en __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 las llamadas operativas; ambas importaciones no están disponibles durante la instanciación ni el descubrimiento de metadatos estáticos. Los marcadores de secreto anidados, falsos o no booleanos y las propiedades secretas cuyo tipo no sea cadena se rechazan. Se rechaza un esquema sin config_read, o un config_read sin esquema.
signaturenoFirma Ed25519 en Base64url sobre los bytes canónicos del manifiesto. Se establece al firmar para distribución.
publisher_keynoClave pública Ed25519 codificada en hexadecimal del firmante.

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.

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.

Para este complemento: name y version deben coincidir con lo que informa plugin-info, wasm_path debe indicar el archivo de componente que distribuirás junto a él, capabilities debe contener exactamente tool y permissions debe contener exactamente config_read. Añade http_client solo si tu herramienta realiza llamadas HTTP salientes. El adaptador de herramientas implementa wasi:http, pero solo lo enlaza después de validar esa concesión; sin la compatibilidad del adaptador y la concesión no hay ninguna superficie HTTP.

El contrato de manifiesto correspondiente a RedactConfig tipado es:

name = "my-redact-plugin"
version = "0.1.0"
wasm_path = "my_redact_plugin.wasm"
capabilities = ["tool"]
permissions = ["config_read"]

[config_schema]
"$schema" = "https://json-schema.org/draft/2020-12/schema"
type = "object"
additionalProperties = false

[config_schema.properties.replacement]
type = "string"
minLength = 1

[config_schema.properties.redact_emails]
type = "boolean"

[config_schema.properties.patterns]
type = "array"
items = { type = "string" }

Estas propiedades son opcionales y coinciden con los valores predeterminados del invitado. Para una credencial que deba existir, añade su nombre a required en [config_schema]; una concesión denegada o un valor ausente impedirá que el componente se inicie.

Herramientas que llaman a la red

Podría decirse que el tipo de herramienta más común en el mundo real no es una transformación pura como redact, sino un puente a una API externa: declara http_client en el manifiesto, lee las credenciales mediante el servicio de secretos con ámbito y realiza una solicitud saliente. Marca la credencial en el esquema firmado:

[config_schema]
required = ["api_key"]

[config_schema.properties.api_key]
type = "string"
minLength = 1
x-secret = true

La pieza que falta con respecto a esta guía es un cliente HTTP que funcione dentro de un componente: reqwest y compañía no sirven, porque no hay una interfaz de sockets, solo wasi:http. Un cliente conocido por funcionar con este host es waki, que es bloqueante y, por tanto, encaja directamente con la firma síncrona de execute. Añádelo condicionado al destino del componente para que tus módulos de lógica pura sigan siendo comprobables de forma nativa:

cargo add waki --target 'cfg(target_family = "wasm")'

La forma de una llamada, dentro de execute después de analizar la __config pública:

#![allow(unused)]
fn main() {
let api_key = zeroclaw::plugin::secrets::get("api_key")
    .map_err(|_| api_key no está disponible.to_string())?;
let resp = waki::Client::new()
    .get("https://api.example.com/search")
    .query([("q", term.as_str())])
    .header("Autorización", format!("Bearer {api_key}"))
    .connect_timeout(std::time::Duration::from_secs(5))
    .send()
    .map_err(|e| format!("la solicitud falló: {e}"))?;
}

Dos hechos de versiones que parecen una rotura pero no lo son: waki incluye su propio wit-bindgen (0.34) junto con el 0.46 que usan tus bindings de mundo; ambos coexisten y cada uno genera sus propios bindings. Y waki emite imports wasi:http@0.2.4 mientras que la base actual de la toolchain es @0.2.6; el host enlaza ambos sin problema. Ninguno requiere acción.

Recuerda el marco de confianza de la visión general: http_client es todo o nada. El sandbox no limita a dónde envía datos un plugin concedido, así que los operadores que ejecutan una política de firma strict confían en tu código, no en una lista de अनुमति de URL.

6. Prueba la lógica de forma nativa

Como redact.rs no tiene dependencia de wasm, cargo test normal lo cubre en el host:

#![allow(unused)]
fn main() {
#[test]
fn empty_config_falls_back_to_defaults() {
    let cfg: RedactConfig = serde_json::from_str("{}").unwrap();
    let (out, n) = redact("mail me at a@b.example", &cfg);
    assert_eq!(n, 1);
    assert!(out.contains("[REDACTED]"));
}
}

Cubra como mínimo: el caso de jail (sección vacía), el caso configurado y el paso directo limpio del texto sin nada que enmascarar. Cada comportamiento que el glue reenvía debe poder probarse aquí sin una toolchain de wasm a la vista.

7. Compilar

Instala el objetivo WASI Preview 2 una vez y, luego, compila el componente:

rustup target add wasm32-wasip2
cargo build --release --target wasm32-wasip2

El componente se encuentra en target/wasm32-wasip2/release/<crate_name>.wasm (los guiones en el nombre del crate se convierten en guiones bajos). Cámbiale el nombre a lo que declare tu wasm_path en el manifiesto cuando ensambles el directorio del plugin.

[!IMPORTANTE] Los archivos .wasm y .cwasm compilados son artefactos binarios, a menudo de varios megabytes cada uno. No los incluyas en un árbol de código fuente de git sin Git LFS: cada recompilación confirmada como un blob plano infla el historial del repositorio de forma permanente, y las herramientas de git diff/revisión no pueden con ellos. Trátalos como cualquier otro resultado de compilación: añade target/ y *.wasm/*.cwasm a .gitignore, y distribúyelos mediante un artefacto de lanzamiento o un archivo del registro de plugins en su lugar. Si un artefacto realmente debe vivir en el árbol, registra el patrón con LFS (git lfs track "*.wasm") antes del primer commit.

Si el host de destino es una compilación solo de ejecución (sin backend JIT compilado), no puede compilar .wasm al cargar; en su lugar, deserializa un .cwasm precompilado. Precompílalo con una CLI de wasmtime cuya versión coincida con la del host y distribuye el .cwasm como el artefacto wasm_path. Un artefacto con versión no coincidente es rechazado por la comprobación de deserialización de wasmtime, no se carga incorrectamente de forma silenciosa.

8. Instalar y verificar

Estos comandos requieren un binario compilado con el host de plugins integrado. Los binarios de lanzamiento precompilados que distribuye el instalador se compilan sin la característica plugins-wasm, por lo que zeroclaw plugin ... es un subcomando no reconocido y los plugins instalados nunca se descubren. Compila desde el código fuente con un backend de ejecución de plugins, p. ej. cargo build --release --features plugins-wasm-cranelift.

Cada complemento vive en su propio subdirectorio del directorio de plugins (predeterminado ~/.zeroclaw/plugins/, resuelto mediante plugins.plugins_dir), que contiene el manifiesto y el componente nombrado para coincidir con wasm_path:

~/.zeroclaw/plugins/
└── my-plugin/
    ├── manifest.toml
    └── my-plugin.wasm

Instala desde un directorio local (esto valida la forma del manifiesto y ejecuta la directiva de firmas antes de copiar nada):

zeroclaw plugin install ./my-plugin/

Habilita el sistema de plugins y confirma la detección:

zeroclaw config set plugins.enabled true
zeroclaw plugin list
zeroclaw plugin info my-plugin

zeroclaw plugin list y zeroclaw plugin info confirman que un paquete está instalado y se puede descubrir, pero el descubrimiento no implica la activación. plugins.enabled = true activa el host de plugins; las capacidades de herramientas y habilidades descubiertas automáticamente se cargan en tiempo de ejecución solo cuando plugins.auto_discover = true también está habilitado, y ese indicador es false de forma predeterminada (cierre ante fallos):

zeroclaw config set plugins.auto_discover true

Por sí solo, plugins.enabled = true te proporciona los canales que declares en [channels.plugin.<alias>] y ninguna herramienta ni habilidad de plugin: un paquete de herramientas o habilidades puede aparecer en zeroclaw plugin list y, aun así, no aportar nada en tiempo de ejecución. Las vinculaciones explícitas de canales reciben su nombre del operador en lugar de descubrirse automáticamente, por lo que no necesitan auto_discover; la opción controla únicamente las herramientas y habilidades descubiertas automáticamente.

Se omitió en el descubrimiento un plugin que faltaba en zeroclaw plugin list: consulta el registro de inicio para ver la advertencia de omisión (manifiesto mal formado, archivo wasm_path faltante o rechazo por la política de firmas).

9. Ejecútalo

Pide al agente que use la herramienta:

> redact this before you log it: key sk-live-abc123, mail ops@example.com

El modelo ve redact en su catálogo con tu esquema, lo invoca y el host ejecuta el componente en un almacén nuevo bajo los límites configurados de combustible y memoria. Las herramientas del plugin no están en el conjunto integrado de autoaprobación de solo lectura, así que, en autonomía no total, la llamada muestra el aviso de aprobación del operador como cualquier otra herramienta privilegiada; anticípalo en la descripción de tu herramienta en lugar de sorprenderte por ello. Tus eventos log-record aparecen en el registro estructurado con la atribución de span del sitio de llamada del host.

Dos restricciones operativas que vale la pena repetir del resumen de plugins:

  • Los nombres de las herramientas no deben colisionar con los integrados. Las herramientas integradas se registran primero y la resolución de despacho toma la primera coincidencia (find_tool en el runtime), por lo que una herramienta de plugin con un nombre como el de una integrada nunca se selecciona. No hay ningún error; simplemente no ocurre nada. Elige un nombre único.
  • Una herramienta por componente. El mundo tool-plugin exporta una única interfaz tool. Un toolbox es varios directorios de plugins, uno por componente.

Solución de problemas

SíntomaCausa probable
Falta el plugin en zeroclaw plugin listSistema de plugins deshabilitado; manifest inválido; falta el archivo wasm_path; la política de firma lo rechazó. El registro de inicio incluye la advertencia específica de omisión.
Aparece en zeroclaw plugin list, pero la herramienta nunca se cargaplugins.auto_discover es false (el valor predeterminado). Las capacidades de herramientas y habilidades detectadas automáticamente se cargan solo cuando plugins.auto_discover = true; plugins.enabled = true por sí solo activa únicamente los canales declarados explícitamente. Ejecuta zeroclaw config set plugins.auto_discover true.
Herramienta rechazada durante el registroLa validación de la configuración o el sondeo de metadatos falló. Consulta el registro para ver el error específico; un error del sondeo suele significar que el componente se compiló con un WIT incompatible.
La herramienta nunca fue seleccionada por el modeloEl nombre entra en conflicto con una función integrada, o la descripción/el esquema no le indican al modelo cuándo se aplica la herramienta.
__config ausente a pesar de la sección configuradaEl ámbito efectivo denegó config_read, la entrada no usa la clave de instancia completa mostrada durante la instalación, el objeto validado está vacío o todas las propiedades validadas están marcadas como secretas. En cambio, una discrepancia entre config_schema y los permisos rechaza el complemento.
secrets.get devuelve not-foundLa propiedad falta o no es una cadena directa de nivel superior marcada como x-secret = true en el esquema admitido.
secrets.get devuelve unavailableLa llamada se ejecutó fuera de execute, la resolución de la configuración falló o la ejecución agotó su presupuesto fijo de llamadas al host.
La llamada falla o produce una trampaSe ha alcanzado el límite de combustible, tiempo de reloj o memoria. Aumenta plugins.limits.call_fuel, plugins.limits.call_timeout_ms o plugins.limits.max_memory_mb según corresponda, o haz menos en cada llamada.
La carga falla en un host solo de tiempo de ejecuciónEnvíaste .wasm a un host sin JIT; en su lugar, envía una .cwasm que coincida con la versión.

Siguiente