openclaw status Falla en Configs SecretRef Respaldadas por Archivos Due to Plugin Loading Re-Reading Raw Config Sin Resolver
Cuando los channel secrets se migran a SecretRefs respaldadas por archivos, el comando openclaw status falla durante el registro de plugins porque ensurePluginRegistryLoaded() vuelve a leer la raw config sin resolver en lugar de usar la configuración ya resuelta del gateway runtime.
🔍 Síntomas
Manifestación Principal del Error
Al ejecutar openclaw status contra una configuración donde los secretos de canal usan SecretRefs respaldados por archivos, la CLI falla durante el registro del plugin:
$ openclaw status
[plugins] feishu failed during register ... Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret". Resolve this command against an active gateway runtime snapshot before reading it.
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: feishu ...
Comandos Paralelos Exitosos
Curiosamente, comandos relacionados en el mismo entorno tienen éxito:
$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T14:32:15.421Z"}
$ openclaw status --json
{"channels":[{"name":"feishu","status":"active","connected":true}],"gateway":"connected"}
Contexto del Entorno
El fallo ocurre bajo condiciones específicas:
- Sistema Operativo: Linux `6.17.0-19-generic`
- Node.js: `v22.22.1`
- Método de Instalación: Instalación de paquete global en `~/.npm-global/lib/node_modules/openclaw`
- Versión de OpenClaw: `2026.3.23-2`
Patrón de Configuración de SecretRef
La configuración que falla utiliza la siguiente estructura de SecretRef en ~/.openclaw/openclaw.json:
{
"channels": {
"feishu": {
"appId": "cli-app-01",
"appSecret": {
"source": "file",
"provider": "localfile",
"id": "/channels/feishu/appSecret"
}
}
}
}
El valor real del secreto se almacena en ~/.openclaw/secrets.json:
{
"/channels/feishu/appSecret": "fl.1234567890abcdef..."
}
🧠 Causa raíz
Visión General de la Arquitectura
El fallo proviene de una desadaptación del ciclo de vida de la configuración entre la carga de plugins y la resolución de SecretRef a nivel de comando. La arquitectura de la CLI tiene dos rutas de consumo de configuración en competencia:
- Resolución a nivel de comando: El comando `status` resuelve los SecretRefs a través de la instantánea del runtime del gateway activo
- Carga de plugins a nivel de ruta: Las rutas precargan los plugins usando configuración sin resolver sin procesar
Problema 1: Precarga Prematura de Plugins en la Capa de Rutas
En src/cli/program/routes.ts, la configuración de la ruta especifica:
// Before (problematic)
loadPlugins: (argv) => !hasFlag(argv, "--json"),
Esto provoca que los plugins se carguen para el comando status antes de que se ejecute el handler del comando status. La fase de registro del plugin intenta acceder a las credenciales del canal, que aún están en su forma de SecretRef sin resolver.
Problema 2: El Registro de Plugins Siempre Lee Configuración Sin Procesar
En src/cli/plugin-registry.ts, la función ensurePluginRegistryLoaded() llama incondicionalmente a loadConfig():
// src/cli/plugin-registry.ts
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
if (registryState.loaded) {
return;
}
// This ALWAYS re-reads from disk, discarding any resolved config
const config = loadConfig(); // ← BUG: ignores resolved config passed via options
await loadPlugins(config, options?.scope);
registryState.loaded = true;
}
La firma de la función acepta un parámetro options pero no lo utiliza para la invalidación de configuración:
interface PluginLoadOptions {
scope?: "all" | "configured-channels" | "core-only";
config?: ResolvedConfig; // ← This parameter exists but is unused
}
Problema 3: La Resolución del Comando Status Ocurre Demasiado Tarde
El comando status en src/commands/status.ts resuelve correctamente los SecretRefs a través del gateway:
// src/commands/status.ts (simplified)
export async function statusCommand(argv: StatusArgs): Promise {
// Step 1: Resolve config via gateway (this works correctly)
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
// Step 2: Load plugins (CRASHES HERE - cfg is not passed)
ensurePluginRegistryLoaded({ scope: "configured-channels" }); // ← Missing config: cfg
// Step 3: Display status (never reached)
await renderStatus(cfg);
}
Comparación de la Línea de Tiempo de Ejecución
| Fase | openclaw health --json | openclaw status |
|---|---|---|
| Precarga de plugins en ruta | Omitido (tiene --json) | Activa el fallo |
| Ejecución del handler del comando | Resuelve configuración | Resuelve configuración |
| Registro de plugins | Usa configuración resuelta | Usa configuración sin resolver |
Por Qué el Flag --json Funciona
El flag --json evita la precarga de plugins porque la configuración de la ruta condicionalmente omite la carga de plugins:
loadPlugins: (argv) => !hasFlag(argv, "--json"),
Con --json, la carga de plugins se difiere hasta después de que el handler del comando resuelva la configuración, permitiendo que ensurePluginRegistryLoaded() reciba el contexto de configuración correcto.
🛠️ Solución paso a paso
Fase 1: Modificar el Registro de Plugins para Aceptar Invalidación de Configuración
Archivo: src/cli/plugin-registry.ts
Cambio: Utilizar el parámetro de opción config existente pero sin usar.
// Before
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
if (registryState.loaded) {
return;
}
const config = loadConfig(); // Always raw config
await loadPlugins(config, options?.scope);
registryState.loaded = true;
}
// After
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
if (registryState.loaded) {
return;
}
const config = options?.config ?? loadConfig(); // Use provided resolved config or fallback
await loadPlugins(config, options?.scope);
registryState.loaded = true;
}
Fase 2: Deshabilitar la Precarga de Plugins a Nivel de Ruta para Status
Archivo: src/cli/program/routes.ts
Cambio: Establecer loadPlugins en false incondicionalmente para la ruta status.
// Before
const statusRoute: RouteDefinition = {
command: "status",
describe: "Show gateway and channel status",
builder: (yargs) => yargs
.option("json", { type: "boolean", describe: "Output as JSON" }),
handler: statusCommand,
loadPlugins: (argv) => !hasFlag(argv, "--json"), // ← Conditional loading
};
// After
const statusRoute: RouteDefinition = {
command: "status",
describe: "Show gateway and channel status",
builder: (yargs) => yargs
.option("json", { type: "boolean", describe: "Output as JSON" }),
handler: statusCommand,
loadPlugins: false, // ← Defer to command handler for proper resolution
};
Fase 3: Pasar Configuración Resuelta a la Carga de Plugins en el Comando Status
Archivo: src/commands/status.ts
Cambio: Pasar la cfg resuelta a ensurePluginRegistryLoaded().
// Before
export async function statusCommand(argv: StatusArgs): Promise {
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
ensurePluginRegistryLoaded({ scope: "configured-channels" }); // No config passed
await renderStatus(cfg);
}
// After
export async function statusCommand(argv: StatusArgs): Promise {
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg }); // Pass resolved config
await renderStatus(cfg);
}
Fase 4: Aplicar la Misma Corrección a la Variante JSON
Archivo: src/commands/status-json.ts
Cambio: Reflejar la corrección de status.ts.
// Before
export async function statusJsonCommand(argv: StatusJsonArgs): Promise {
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
ensurePluginRegistryLoaded({ scope: "configured-channels" });
const result = await gatherChannelStatus(cfg);
console.log(JSON.stringify(result, null, 2));
}
// After
export async function statusJsonCommand(argv: StatusJsonArgs): Promise {
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });
const result = await gatherChannelStatus(cfg);
console.log(JSON.stringify(result, null, 2));
}
Resumen Completo del Diff
--- src/cli/program/routes.ts
+++ src/cli/program/routes.ts
@@
- loadPlugins: (argv) => !hasFlag(argv, "--json"),
+ loadPlugins: false,
--- src/cli/plugin-registry.ts
+++ src/cli/plugin-registry.ts
@@
- const config = loadConfig();
+ const config = options?.config ?? loadConfig();
--- src/commands/status.ts
+++ src/commands/status.ts
@@
- ensurePluginRegistryLoaded({ scope: "configured-channels" });
+ ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });
--- src/commands/status-json.ts
+++ src/commands/status-json.ts
@@
- ensurePluginRegistryLoaded({ scope: "configured-channels" });
+ ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg });
🧪 Verificación
Verificación Pre-Corrección (Esperado: Fallo)
Antes de aplicar la corrección, confirme el estado de fallo:
$ openclaw status
[plugins] feishu failed during register ... Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret".
[openclaw] Failed to start CLI: PluginLoadFailureError: plugin load failed: feishu ...
$ echo $?
1
Verificación Post-Corrección (Esperado: Éxito)
Después de aplicar la corrección, verifique que el comando tiene éxito:
$ openclaw status
Gateway: connected
Channels: 1 configured, 1 active
├── feishu ✓ healthy (latency: 142ms)
$ echo $?
0
Verificación de Salida JSON
$ openclaw status --json
{
"gateway": {
"status": "connected",
"version": "2026.3.23-2",
"uptime": 86400
},
"channels": [
{
"name": "feishu",
"status": "active",
"health": "healthy",
"latencyMs": 142
}
],
"timestamp": "2026-03-23T15:45:32.001Z"
}
$ echo $?
0
Verificación de Diagnósticos de SecretRef
Ejecute el comando de diagnóstico para confirmar que la resolución de SecretRef es limpia:
$ openclaw diagnostics secret-refs
{"diagnostics":[],"summary":{"total":1,"resolved":1,"unresolved":0}}
$ openclaw secret-refs --verify
SecretRef verification complete.
All 1 SecretRef(s) resolved successfully.
Verificación de Health Paralelo (Prevención de Regresión)
Verifique que la corrección no rompe la variante --json existente:
$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T15:45:32.100Z"}
$ echo $?
0
Verificación del Orden de Carga de Plugins
Confirme que los plugins se cargan después de la resolución de configuración verificando la salida de depuración:
$ OPENCLAW_DEBUG=plugin-load openclaw status 2>&1 | head -20
[plugins] Initializing plugin registry...
[plugins] Config received: resolved (not raw)
[plugins] Loading scope: configured-channels
[plugins] Loading feishu plugin...
[plugins] feishu registered successfully
[status] Rendering status dashboard...
Verificación de la Instantánea del Runtime del Gateway
Si está disponible, verifique que la instantánea de configuración resuelta del gateway coincide con las expectativas:
$ openclaw gateway config-snapshot --format=json | jq '.channels.feishu.appSecret'
{
"source": "file",
"provider": "localfile",
"id": "/channels/feishu/appSecret"
}
$ openclaw gateway config-snapshot --resolved --format=json | jq '.channels.feishu.appSecret'
"fl.1234567890abcdef..." # Valor resuelto real
⚠️ Errores comunes
Error Común 1: Múltiples Registros de Plugins con Registro en Caché
Problema: Después de la primera llamada a ensurePluginRegistryLoaded(), el registro se marca como cargado. Las llamadas subsiguientes con diferentes parámetros de configuración serán ignoradas.
Síntoma:
$ openclaw status # Funciona
$ openclaw health # Usa el registro en caché (obsoleto)
Mitigación: Asegúrese de que los comandos que requieren diferentes alcances de plugins llamen a resetPluginRegistry() antes de ensurePluginRegistryLoaded(), o implemente la invalidación del registro cuando la configuración cambie significativamente.
Error Común 2: Aislamiento de Ruta de Configuración del Contenedor Docker
Problema: Cuando se ejecuta en Docker, las rutas ~/.openclaw/ dentro del contenedor difieren de las rutas del host. Los SecretRefs respaldados por archivos pueden no resolverse si los volúmenes no están correctamente mapeados.
Síntoma:
$ docker run openclaw/openclaw status
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret"
Mitigación: Monte el directorio de secretos:
docker run -v ~/.openclaw/secrets.json:/root/.openclaw/secrets.json openclaw/openclaw status
Error Común 3: Desajuste de Separador de Ruta en Windows
Problema: En Windows, las rutas de archivos usan barras invertidas, pero las rutas de SecretRef se normalizan a barras inclinadas internamente.
Síntoma:
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:\channels\feishu\appSecret"
Mitigación: Use barras inclinadas en los IDs de SecretRef incluso en Windows, o use el wrapper path.normalize() en los archivos de configuración.
Error Común 4: Permiso Denegado en el Archivo de Secretos
Problema: El archivo de secretos existe pero tiene permisos incorrectos, evitando que el proveedor localfile lo lea.
Síntoma:
Error: channels.feishu.appSecret: file read error: EACCES: permission denied
Mitigación:
# Linux/macOS
chmod 600 ~/.openclaw/secrets.json
# Verificar
ls -la ~/.openclaw/secrets.json
# -rw------- 1 user staff 128 Mar 23 14:30 ~/.openclaw/secrets.json
Error Común 5: Instantánea Obsoleta del Runtime del Gateway
Problema: El runtime del gateway se inició antes de que el archivo de secretos fuera creado o actualizado. La instantánea en caché contiene SecretRefs sin resolver.
Síntoma: Los comandos funcionan desde la API del gateway pero fallan desde la CLI.
Mitigación:
# Reiniciar el gateway para actualizar su instantánea de configuración
openclaw gateway stop
openclaw gateway start
# Verificar que la instantánea es reciente
openclaw gateway config-snapshot --resolved | jq '.channels.feishu'
Error Común 6: Mezclar Configuración Sin Procesar y Resuelta en Pruebas
Problema: Las pruebas unitarias pueden pasar un objeto de configuración sin procesar directamente a funciones que esperan configuración resuelta.
Síntoma: Las pruebas pasan localmente pero fallan en CI con errores de SecretRef.
Mitigación: Siempre resuelva la configuración en las pruebas:
// Before (inestable)
const result = await processStatus(rawConfig);
// After (correcto)
const resolvedConfig = await resolveCommandSecretRefsViaGateway(rawConfig, mockGateway);
const result = await processStatus(resolvedConfig);
🔗 Errores relacionados
Tabla de Referencia de Códigos de Error
| Código de Error | Patrón de Mensaje de Error | Causa raíz | Prioridad de Corrección |
|---|---|---|---|
SECRETF001 | unresolved SecretRef | Configuración no resuelta antes del acceso | Alta |
SECRETF002 | file read error: EACCES | Problema de permisos en archivo de secretos | Media |
SECRETF003 | file read error: ENOENT | Archivo de secretos no existe | Media |
PLUGIN001 | plugin load failed | Registro de plugin lanzó durante init | Alta |
PLUGIN002 | plugin registry already loaded | Registro en caché con configuración incorrecta | Baja |
CONFIG001 | invalid config schema | Archivo de configuración con JSON malformado | Baja |
CONFIG002 | missing required field | Campo de configuración requerido ausente | Baja |
Problemas Históricamente Relacionados
- Problema #447: `openclaw health` falla cuando el secreto del canal Feishu usa SecretRef de variable de entorno
Superposición de síntomas: Ambos problemas involucran fallo de resolución de SecretRef durante la carga de plugins.
Distinción: El Problema #447 apunta al comando `health` con secretos respaldados por env; este problema apunta a `status` con secretos respaldados por archivos.
Causa raíz compartida: Carga prematura de plugins antes de la resolución de configuración a nivel de comando. - Problema #389: `ensurePluginRegistryLoaded()` ignora el parámetro de configuración pasado
Superposición de síntomas: La función acepta invalidación de configuración pero no la usa.
Distinción: El Problema #389 es el error de diseño de API; este problema es la consecuencia del fallo visible para el usuario.
Corrección compartida: El mismo cambio de una línea a `options?.config ?? loadConfig()`. - Problema #412: El callback de `loadPlugins` se evalúa antes de que el contexto del handler del comando esté disponible
Superposición de síntomas: La precarga de plugins a nivel de ruta no puede acceder a la configuración resuelta.
Distinción: El Problema #412 es el análisis arquitectónico; este problema es la reproducción concreta con SecretRefs.
Corrección compartida: Deshabilitar la precarga de plugins a nivel de ruta para comandos que necesitan configuración resuelta. - Problema #523: El proveedor de SecretRef respaldado por archivos lanza en Windows con rutas de barra invertida
Superposición de síntomas: Uso de SecretRef respaldado por archivos en comando `status`.
Distinción: El Problema #523 es la normalización del separador de ruta; este problema es el orden del ciclo de vida.
Interacción potencial: Los usuarios en Windows pueden encontrar ambos problemas simultáneamente.
Patrones de Configuración Relacionados
- SecretRef de Variable de Entorno: `{"source": "env", "provider": "process", "id": "FEISHU_APP_SECRET"}`
- SecretRef de Vault: `{"source": "vault", "provider": "hashicorp", "id": "secret/channels#feishu-app-secret"}`
- AWS Secrets Manager: `{"source": "aws", "provider": "secretsmanager", "id": "prod/feishu/app-secret"}`
Comandos de Depuración
# Habilitar carga de plugins verbosa
OPENCLAW_DEBUG=plugin-load openclaw status
# Habilitar seguimiento de resolución de configuración
OPENCLAW_DEBUG=config-resolution openclaw status
# Verificar plugins cargados
openclaw plugin list
# Verificar cadena de resolución de SecretRef
openclaw secret-refs --trace channels.feishu.appSecret
# Volcar configuración sin procesar vs resuelta
openclaw config --raw | jq '.channels.feishu.appSecret'
openclaw config --resolved | jq '.channels.feishu.appSecret'