openclaw status schlägt bei dateibasierten SecretRef-Konfigurationen fehl wegen Plugin Loading, das unaufgelöste Raw Config erneut einliest
Wenn Kanal-Geheimnisse (channel secrets) zu dateibasierten SecretRefs migriert werden, stürzt der `openclaw status`-Befehl während der Plugin-Registrierung ab, weil `ensurePluginRegistryLoaded()` die unaufgelöste Raw Config erneut einliest, anstatt die bereits aufgelöste Konfiguration aus der Gateway-Runtime zu verwenden.
🔍 Symptome
Primäre Fehlermanifestation
Beim Ausführen von openclaw status gegen eine Konfiguration, bei der Channel-Secrets dateibasierte SecretRefs verwenden, stürzt die CLI während der Plugin-Registrierung ab:
$ 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 ...
Erfolgreiche parallele Befehle
Interessanterweise sind verwandte Befehle in derselben Umgebung erfolgreich:
$ 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"}
Umgebungskontext
Der Fehler tritt unter bestimmten Bedingungen auf:
- OS: Linux `6.17.0-19-generic`
- Node.js: `v22.22.1`
- Installationsmethode: Globale Paketinstallation unter `~/.npm-global/lib/node_modules/openclaw`
- OpenClaw Version: `2026.3.23-2`
SecretRef-Konfigurationsmuster
Die fehlerhafte Konfiguration verwendet die folgende SecretRef-Struktur in ~/.openclaw/openclaw.json:
{
"channels": {
"feishu": {
"appId": "cli-app-01",
"appSecret": {
"source": "file",
"provider": "localfile",
"id": "/channels/feishu/appSecret"
}
}
}
}
Der tatsächliche geheime Wert wird in ~/.openclaw/secrets.json gespeichert:
{
"/channels/feishu/appSecret": "fl.1234567890abcdef..."
}
🧠 Ursache
Architektonische Übersicht
Der Fehler resultiert aus einer Konfigurations-Lebenszyklus-Diskrepanz zwischen Plugin-Laden und befehlsbezogener SecretRef-Auflösung. Die CLI-Architektur hat zwei konkurrierende Konfigurations-Verbrauchspfade:
- Befehlsbezogene Auflösung: Der `status`-Befehl löst SecretRefs über aktive Gateway-Laufzeit-Snapshot auf
- Routenzug-Ebene Plugin-Laden: Routen laden Plugins unter Verwendung roher, unaufgelöster Konfiguration vor
Problem 1: Vorzeitiges Plugin-Vorladen in der Routenzug-Ebene
In src/cli/program/routes.ts gibt die Routenkonfiguration an:
// Vorher (problematisch)
loadPlugins: (argv) => !hasFlag(argv, "--json"),
Dies führt dazu, dass Plugins für den status-Befehl bevor der status-Befehlshandler ausgeführt wird, geladen werden. Die Plugin-Registrierungsphase versucht auf Channel-Anmeldedaten zuzugreifen, die sich noch in ihrer unaufgelösten SecretRef-Form befinden.
Problem 2: Plugin-Registrierung liest immer unaufgelöste Konfiguration
In src/cli/plugin-registry.ts ruft die Funktion ensurePluginRegistryLoaded() bedingungslos loadConfig() auf:
// src/cli/plugin-registry.ts
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
if (registryState.loaded) {
return;
}
// Dies LIEST IMMER vom Datenträger und verwirft alle aufgelösten Konfigurationen
const config = loadConfig(); // ← FEHLER: ignoriert aufgelöste Konfiguration aus Optionen
await loadPlugins(config, options?.scope);
registryState.loaded = true;
}
Die Funktionssignatur akzeptiert einen options-Parameter, nutzt ihn jedoch nicht für Konfigurationsüberschreibung:
interface PluginLoadOptions {
scope?: "all" | "configured-channels" | "core-only";
config?: ResolvedConfig; // ← Dieser Parameter existiert, wird aber nicht verwendet
}
Problem 3: Status-Befehl-Auflösung geschieht zu spät
Der status-Befehl in src/commands/status.ts löst SecretRefs korrekt über Gateway auf:
// src/commands/status.ts (vereinfacht)
export async function statusCommand(argv: StatusArgs): Promise {
// Schritt 1: Konfiguration über Gateway auflösen (funktioniert korrekt)
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
// Schritt 2: Plugins laden (ABSTURZ HIER - cfg wird nicht übergeben)
ensurePluginRegistryLoaded({ scope: "configured-channels" }); // ← config: cfg fehlt
// Schritt 3: Status anzeigen (nie erreicht)
await renderStatus(cfg);
}
Vergleich der Ausführungszeitleiste
| Phase | openclaw health --json | openclaw status |
|---|---|---|
| Routenzug-Plugin-Vorladen | Übersprungen (hat --json) | Löst Absturz aus |
| Befehlshandler-Ausführung | Löst Konfiguration auf | Löst Konfiguration auf |
| Plugin-Registrierung | Verwendet aufgelöste Konfiguration | Verwendet unaufgelöste Konfiguration |
Warum das Flag --json funktioniert
Das Flag --json umgeht das Routenzug-Plugin-Vorladen, da die Routenkonfiguration das Plugin-Laden bedingt überspringt:
loadPlugins: (argv) => !hasFlag(argv, "--json"),
Mit --json wird das Plugin-Laden verzögert, bis der Befehlshandler die Konfiguration aufgelöst hat, sodass ensurePluginRegistryLoaded() den korrekten Konfigurationskontext erhält.
🛠️ Schritt-für-Schritt-Lösung
Phase 1: Plugin-Registrierung zur Annahme von Konfigurationsüberschreibung ändern
Datei: src/cli/plugin-registry.ts
Änderung: Nutzen Sie den vorhandenen, aber ungenutzten config-Optionsparameter.
// Vorher
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
if (registryState.loaded) {
return;
}
const config = loadConfig(); // Immer unaufgelöste Konfiguration
await loadPlugins(config, options?.scope);
registryState.loaded = true;
}
// Nachher
export async function ensurePluginRegistryLoaded(options?: PluginLoadOptions): Promise {
if (registryState.loaded) {
return;
}
const config = options?.config ?? loadConfig(); // Aufgelöste Konfiguration verwenden oder Fallback
await loadPlugins(config, options?.scope);
registryState.loaded = true;
}
Phase 2: Routenzug-Plugin-Vorladen für Status deaktivieren
Datei: src/cli/program/routes.ts
Änderung: Setzen Sie loadPlugins für die Status-Route bedingungslos auf false.
// Vorher
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"), // ← Bedingtes Laden
};
// Nachher
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, // ← Verzögerung an Befehlshandler für korrekte Auflösung
};
Phase 3: Aufgelöste Konfiguration an Plugin-Laden im Status-Befehl übergeben
Datei: src/commands/status.ts
Änderung: Übergeben Sie die aufgelöste cfg an ensurePluginRegistryLoaded().
// Vorher
export async function statusCommand(argv: StatusArgs): Promise {
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
ensurePluginRegistryLoaded({ scope: "configured-channels" }); // Keine Konfiguration übergeben
await renderStatus(cfg);
}
// Nachher
export async function statusCommand(argv: StatusArgs): Promise {
const cfg = await resolveCommandSecretRefsViaGateway(rawConfig, gateway);
ensurePluginRegistryLoaded({ scope: "configured-channels", config: cfg }); // Aufgelöste Konfiguration übergeben
await renderStatus(cfg);
}
Phase 4: Dieselbe Lösung auf JSON-Variante anwenden
Datei: src/commands/status-json.ts
Änderung: Spiegeln Sie die Lösung aus status.ts.
// Vorher
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));
}
// Nachher
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));
}
Vollständige Diff-Zusammenfassung
--- 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 });
🧪 Verifizierung
Vor-Fix-Verifizierung (Erwartet: Fehler)
Bevor Sie die Lösung anwenden, bestätigen Sie den Fehlerzustand:
$ 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
Nach-Fix-Verifizierung (Erwartet: Erfolg)
Nach Anwendung der Lösung verifizieren Sie, dass der Befehl erfolgreich ist:
$ openclaw status
Gateway: connected
Channels: 1 configured, 1 active
├── feishu ✓ healthy (latency: 142ms)
$ echo $?
0
JSON-Ausgabe-Verifizierung
$ 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
Secret-Diagnose-Verifizierung
Führen Sie den Diagnosebefehl aus, um zu bestätigen, dass die SecretRef-Auflösung sauber ist:
$ 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.
Parallele Gesundheitsprüfung (Regression verhindern)
Verifizieren Sie, dass die Lösung die vorhandene --json-Variante nicht beeinträchtigt:
$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T15:45:32.100Z"}
$ echo $?
0
Plugin-Ladereihenfolge-Verifizierung
Bestätigen Sie, dass Plugins nach der Konfigurationsauflösung laden, indem Sie die Debug-Ausgabe überprüfen:
$ 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...
Gateway-Laufzeit-Snapshot-Verifizierung
Wenn verfügbar, verifizieren Sie, dass die aufgelöste Konfigurations-Snapshot des Gateways den Erwartungen entspricht:
$ 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..." # Tatsächlicher aufgelöster Wert
⚠️ Häufige Fehler
Fehler 1: Mehrfache Plugin-Registrierungen mit gecachtem Registry
Problem: Nach dem ersten Aufruf von ensurePluginRegistryLoaded() wird die Registry als geladen markiert. Nachfolgende Aufrufe mit unterschiedlichen Konfigurationsparametern werden ignoriert.
Symptom:
$ openclaw status # Funktioniert
$ openclaw health # Verwendet gecachte (veraltete) Registry
Abhilfe: Stellen Sie sicher, dass Befehle, die unterschiedliche Plugin-Scopes erfordern, resetPluginRegistry() vor ensurePluginRegistryLoaded() aufrufen, oder implementieren Sie Registry-Invalidierung bei erheblichen Konfigurationsänderungen.
Fehler 2: Docker-Container-Konfigurationspfad-Isolation
Problem: Bei Ausführung in Docker unterscheiden sich ~/.openclaw/-Pfade innerhalb des Containers von Host-Pfaden. Dateibasierte SecretRefs werden möglicherweise nicht aufgelöst, wenn Volumes nicht korrekt zugeordnet sind.
Symptom:
$ docker run openclaw/openclaw status
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret"
Abhilfe: Binden Sie das Secrets-Verzeichnis ein:
docker run -v ~/.openclaw/secrets.json:/root/.openclaw/secrets.json openclaw/openclaw status
Fehler 3: Windows-Pfadtrennzeichen-Mismatch
Problem: Unter Windows verwenden Dateipfade umgekehrte Schrägstriche, aber SecretRef-Pfade werden intern auf Vorwärtsschrägstriche normalisiert.
Symptom:
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:\channels\feishu\appSecret"
Abhilfe: Verwenden Sie Vorwärtsschrägstriche in SecretRef-IDs auch unter Windows, oder verwenden Sie den path.normalize()-Wrapper in Konfigurationsdateien.
Fehler 4: Zugriff verweigert auf Secrets-Datei
Problem: Die Secrets-Datei existiert, hat aber falsche Berechtigungen, die verhindern, dass der localfile-Provider sie liest.
Symptom:
Error: channels.feishu.appSecret: file read error: EACCES: permission denied
Abhilfe:
# Linux/macOS
chmod 600 ~/.openclaw/secrets.json
# Verifizieren
ls -la ~/.openclaw/secrets.json
# -rw------- 1 user staff 128 Mar 23 14:30 ~/.openclaw/secrets.json
Fehler 5: Veraltete Gateway-Laufzeit-Snapshot
Problem: Die Gateway-Laufzeit wurde gestartet, bevor die Secrets-Datei erstellt oder aktualisiert wurde. Der gecachte Snapshot enthält unaufgelöste SecretRefs.
Symptom: Befehle funktionieren über die Gateway-API, schlagen aber über CLI fehl.
Abhilfe:
# Gateway neu starten, um den Konfigurations-Snapshot zu aktualisieren
openclaw gateway stop
openclaw gateway start
# Verifizieren, dass der Snapshot aktuell ist
openclaw gateway config-snapshot --resolved | jq '.channels.feishu'
Fehler 6: Roh- und aufgelöste Konfiguration in Tests mischen
Problem: Unit-Tests übergeben möglicherweise ein unaufgelöstes Konfigurationsobjekt direkt an Funktionen, die aufgelöste Konfiguration erwarten.
Symptom: Tests bestehen lokal, schlagen aber in CI mit SecretRef-Fehlern fehl.
Abhilfe: Lösen Sie Konfiguration in Tests immer auf:
// Vorher (fehleranfällig)
const result = await processStatus(rawConfig);
// Nachher (korrekt)
const resolvedConfig = await resolveCommandSecretRefsViaGateway(rawConfig, mockGateway);
const result = await processStatus(resolvedConfig);
🔗 Zugehörige Fehler
Fehlercode-Referenztabelle
| Fehlercode | Fehlermeldungsmuster | Ursache | Lösungs-Priorität |
|---|---|---|---|
SECRETF001 | unresolved SecretRef | Konfiguration vor Zugriff nicht aufgelöst | Hoch |
SECRETF002 | file read error: EACCES | Berechtigungsproblem bei Secrets-Datei | Mittel |
SECRETF003 | file read error: ENOENT | Secrets-Datei existiert nicht | Mittel |
PLUGIN001 | plugin load failed | Plugin-Registrierung warf während Init | Hoch |
PLUGIN002 | plugin registry already loaded | Registry mit falscher Konfiguration gecacht | Niedrig |
CONFIG001 | invalid config schema | Konfigurationsdatei fehlerhaftes JSON | Niedrig |
CONFIG002 | missing required field | Erforderliches Konfigurationsfeld fehlt | Niedrig |
Historisch zugehörige Probleme
- Problem #447: `openclaw health` schlägt fehl, wenn Feishu-Channel-Secret Umgebungsvariable SecretRef verwendet
Symptom-Überlappung: Beide Probleme betreffen SecretRef-Auflösungsfehler während des Plugin-Ladens.
Unterscheidung: Problem #447 zielt auf den `health`-Befehl mit env-gestützten Secrets ab; dieses Problem zielt auf `status` mit dateigestützten Secrets.
Gemeinsame Ursache: Vorzeitiges Plugin-Laden vor befehlsbezogener Konfigurationsauflösung. - Problem #389: `ensurePluginRegistryLoaded()` ignoriert übergebenen Konfigurationsparameter
Symptom-Überlappung: Funktion akzeptiert Konfigurationsüberschreibung, verwendet sie aber nicht.
Unterscheidung: Problem #389 ist der API-Design-Fehler; dieses Problem ist die benutzerorientierte Fehlerfolge.
Gemeinsame Lösung: Dieselbe Einzeilenänderung zu `options?.config ?? loadConfig()`. - Problem #412: `loadPlugins`-Callback wird ausgewertet, bevor Befehlshandler-Kontext verfügbar ist
Symptom-Überlappung: Routenzug-Ebene Plugin-Vorladen kann nicht auf aufgelöste Konfiguration zugreifen.
Unterscheidung: Problem #412 ist die architektonische Analyse; dieses Problem ist die konkrete Reproduktion mit SecretRefs.
Gemeinsame Lösung: Deaktivieren Sie Routenzug-Ebene Plugin-Vorladen für Befehle, die aufgelöste Konfiguration benötigen. - Problem #523: Dateigestützter SecretRef-Provider wirft bei Windows mit umgekehrten Schrägstrichen
Symptom-Überlappung: Dateigestützte SecretRef-Verwendung in `status`-Befehl.
Unterscheidung: Problem #523 betrifft Pfadtrennzeichen-Normalisierung; dieses Problem betrifft Lebenszyklus-Reihenfolge.
Mögliche Wechselwirkung: Benutzer unter Windows können beide Probleme gleichzeitig erleben.
Zugehörige Konfigurationsmuster
- Umgebungsvariable SecretRef: `{"source": "env", "provider": "process", "id": "FEISHU_APP_SECRET"}`
- Vault SecretRef: `{"source": "vault", "provider": "hashicorp", "id": "secret/channels#feishu-app-secret"}`
- AWS Secrets Manager: `{"source": "aws", "provider": "secretsmanager", "id": "prod/feishu/app-secret"}`
Debug-Befehle
# Ausführliches Plugin-Laden aktivieren
OPENCLAW_DEBUG=plugin-load openclaw status
# Konfigurationsauflösungs-Verfolgung aktivieren
OPENCLAW_DEBUG=config-resolution openclaw status
# Geladene Plugins überprüfen
openclaw plugin list
# SecretRef-Auflösungssequenz verifizieren
openclaw secret-refs --trace channels.feishu.appSecret
# Unaufgelöste vs. aufgelöste Konfiguration ausgeben
openclaw config --raw | jq '.channels.feishu.appSecret'
openclaw config --resolved | jq '.channels.feishu.appSecret'