[ファイルバックドSecretRef設定でopenclaw statusが失敗する - プラグイン読み込み時に未解決の生の設定を再読み込みするため] - openclaw status Fails on File-Backed SecretRef Configs Due to Plugin Loading Re-Reading Unresolved Raw Config
チャネルシークレットがファイルバックドのSecretRefに移行されると、`openclaw status`コマンドはプラグイン登録中にクラッシュします。これは`ensurePluginRegistryLoaded()`がゲートウェイランタイムからの既に解決済みの設定を使用する代わりに、未解決の生の設定を再読み込みするためです。
🔍 症状
主なエラーの発生状況
ファイルバックドSecretRefを使用して channel secrets を使用する設定に対して openclaw status を実行すると、プラグイン登録中にCLIがクラッシュします:
$ 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 ...
正常に動作する並行コマンド
興味深いことに、同じ環境内の関連コマンドは正常に動作します:
$ 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"}
環境コンテキスト
この失敗は以下の特定の条件下で発生します:
- OS: Linux `6.17.0-19-generic`
- Node.js: `v22.22.1`
- インストール方法: `~/.npm-global/lib/node_modules/openclaw` へのグローバルパッケージインストール
- OpenClawバージョン: `2026.3.23-2`
SecretRef設定パターン
失敗する設定では、~/.openclaw/openclaw.json に以下のSecretRef構造を使用します:
{
"channels": {
"feishu": {
"appId": "cli-app-01",
"appSecret": {
"source": "file",
"provider": "localfile",
"id": "/channels/feishu/appSecret"
}
}
}
}
実際のシークレット値は ~/.openclaw/secrets.json に保存されています:
{
"/channels/feishu/appSecret": "fl.1234567890abcdef..."
}
🧠 原因
アーキテクチャの概要
この失敗は、プラグイン読み込みとコマンドレベルのSecretRef解決の間の設定ライフサイクルの不一致に起因します。CLIアーキテクチャには2つの競合する設定消費パスがあります:
- コマンドレベルの解決: `status` コマンドはアクティブなゲートウェイランタイムスナップショット経由でSecretRefを解決する
- ルートレベルのプラグイン読み込み: ルートは未解決の設定を使用してプラグインをプリロードする
問題1: ルートレベルでの過早なプラグインプリロード
src/cli/program/routes.ts では、ルート設定が以下のように指定されています:
// Before (problematic)
loadPlugins: (argv) => !hasFlag(argv, "--json"),
これにより、status コマンドの前にプラグインが読み込まれます。プラグイン登録フェーズでは、まだ未解決のSecretRef形式のままのチャンネル認証情報にアクセスしようとします。
問題2: プラグインレジストリが常に生の設定を読み取る
src/cli/plugin-registry.ts の ensurePluginRegistryLoaded() 関数は、無条件に 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;
}
関数のシグネチャは options パラメータを受け取りしますが、設定の上書きには使用しません:
interface PluginLoadOptions {
scope?: "all" | "configured-channels" | "core-only";
config?: ResolvedConfig; // ← This parameter exists but is unused
}
問題3: Statusコマンド解決の実行タイミングが迟い
src/commands/status.ts の status コマンドは、ゲートウェイ経由でSecretRefを正しく解決します:
// 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);
}
実行タイムライン比較
| フェーズ | openclaw health --json | openclaw status |
|---|---|---|
| ルートプラグインプリロード | スキップ (--json あり) | クラッシュをトリガー |
| コマンドハンドラ実行 | 設定を解決 | 設定を解決 |
| プラグイン登録 | 解決済み設定を使用 | 未解決設定を使用 |
--json フラグが動作する理由
--json フラグは、ルート設定で条件付きにプラグイン読み込みをスキップするため、プラグインのプリロードをバイパスします:
loadPlugins: (argv) => !hasFlag(argv, "--json"),
--json を付けると、コマンドハンドラが設定を解決した後までプラグイン読み込みが遅延され、ensurePluginRegistryLoaded() が正しい設定コンテキストを受け取れるようになります。
🛠️ 解決手順
フェーズ1: 設定の上書きを受け入れるようにプラグインレジストリを修正
ファイル: src/cli/plugin-registry.ts
変更: 既存だが未使用の config オプションパラメータを使用する。
// 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;
}
フェーズ2: Status用のルートレベルプラグインプリロードを無効化
ファイル: src/cli/program/routes.ts
変更: statusルートの loadPlugins を無条件に false に設定する。
// 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
};
フェーズ3: Statusコマンドで解決済み設定をプラグイン読み込みに渡す
ファイル: src/commands/status.ts
変更: 解決済みの cfg を 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);
}
フェーズ4: JSONバリアントに同じ修正を適用
ファイル: src/commands/status-json.ts
変更: 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));
}
完全な差分サマリー
--- 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 });
🧪 検証
修正前の検証(期待結果: 失敗)
修正を適用する前に、失敗状態をことを確認します:
$ 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
修正後の検証(期待結果: 成功)
修正を適用した後、コマンドが成功することを確認します:
$ openclaw status
Gateway: connected
Channels: 1 configured, 1 active
├── feishu ✓ healthy (latency: 142ms)
$ echo $?
0
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
Secret診断の検証
SecretRef解決がクリーンであることを確認するために、診断コマンドを実行します:
$ 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.
並行ヘルスチェック(回帰防止)
修正が既存の --json バリアントを壊していないことを確認します:
$ openclaw health --json
{"status":"healthy","gateway":"connected","timestamp":"2026-03-23T15:45:32.100Z"}
$ echo $?
0
プラグイン読み込み順序の検証
デバッグ出力を確認して、設定解決後にプラグインが読み込まれることを確認します:
$ 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...
ゲートウェイランタイムスナップショットの検証
利用可能な場合、ゲートウェイの解決済み設定スナップショットが期待値と一致することを確認します:
$ 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..." # Actual resolved value
⚠️ よくある落とし穴
落とし穴1: キャッシュされたレジストリによる複数のプラグイン登録
問題: ensurePluginRegistryLoaded() の最初の呼び出し後、レジストリは読み込み済みとしてマークされます異なる設定パラメータでの後続の呼び出しは無視されます。
症状:
$ openclaw status # Works
$ openclaw health # Uses cached (stale) registry
軽減策: 異なるプラグインスコープが必要なコマンドは、ensurePluginRegistryLoaded() の前に resetPluginRegistry() を呼び出すか、設定が大幅に変わった場合にレジストリの無効化を実装します。
落とし穴2: Dockerコンテナのconfigパス隔離
問題: Dockerで実行する場合、コンテナ内の ~/.openclaw/ パスはホストパスが異なります。ボリュームが正しくマッピングされていないと、ファイルバックドSecretRefが解決されない場合があります。
症状:
$ docker run openclaw/openclaw status
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:/channels/feishu/appSecret"
軽減策: secretsディレクトリをマウントします:
docker run -v ~/.openclaw/secrets.json:/root/.openclaw/secrets.json openclaw/openclaw status
落とし穴3: Windowsのパス区切り文字の不一致
問題: Windowsではファイルパスがバックスラッシュを使用しますが、SecretRefパスは内部的にスラッシュに正規化されます。
症状:
Error: channels.feishu.appSecret: unresolved SecretRef "file:localfile:\channels\feishu\appSecret"
軽減策: WindowsでもSecretRef IDにスラッシュを使用するか、設定ファイルで path.normalize() ラッパーを使用します。
落とし穴4: Secretsファイルでの権限エラー
問題: secretsファイルは存在しますが、localfileプロバイダーがそれを読み取れない不明な権限設定になっています。
症状:
Error: channels.feishu.appSecret: file read error: EACCES: permission denied
軽減策:
# Linux/macOS
chmod 600 ~/.openclaw/secrets.json
# Verify
ls -la ~/.openclaw/secrets.json
# -rw------- 1 user staff 128 Mar 23 14:30 ~/.openclaw/secrets.json
落とし穴5: 古いゲートウェイランタイムスナップショット
問題: secretsファイルが作成または更新される前にゲートウェイランタイムが開始されました。キャッシュされたスナップショットには未解決のSecretRefが含まれています。
症状: ゲートウェイAPIからのコマンドは動作するが、CLIからは失敗する。
軽減策:
# Restart the gateway to refresh its config snapshot
openclaw gateway stop
openclaw gateway start
# Verify snapshot is fresh
openclaw gateway config-snapshot --resolved | jq '.channels.feishu'
落とし穴6: テストでの生の設定と解決済み設定の混在
問題: ユニットテストは、解決済み設定を期待する関数に生の設定オブジェクトを直接渡す場合があります。
症状: ローカルではテストがパスするが、CIではSecretRefエラーで失敗する。
軽減策: テストでは常に設定を解決します:
// Before (flaky)
const result = await processStatus(rawConfig);
// After (correct)
const resolvedConfig = await resolveCommandSecretRefsViaGateway(rawConfig, mockGateway);
const result = await processStatus(resolvedConfig);
🔗 関連するエラー
エラーコードリファレンステーブル
| エラーコード | エラーメッセージパターン | 根本原因 | 修正優先度 |
|---|---|---|---|
SECRETF001 | unresolved SecretRef | アクセス前に設定が解決されていない | 高 |
SECRETF002 | file read error: EACCES | secretsファイルの権限問題 | 中 |
SECRETF003 | file read error: ENOENT | secretsファイルが存在しない | 中 |
PLUGIN001 | plugin load failed | 初期化中にプラグイン登録が例外をスロー | 高 |
PLUGIN002 | plugin registry already loaded | 誤った設定でレジストリがキャッシュされた | 低 |
CONFIG001 | invalid config schema | 設定ファイルのJSONが不正 | 低 |
CONFIG002 | missing required field | 必須の設定フィールドが存在しない | 低 |
歴史的な関連issues
- Issue #447: Feishuチャンネルのシークレットが環境変数SecretRefを使用する場合、`openclaw health` が失敗する
症状の重複: 両issueともプラグイン読み込み中のSecretRef解決失敗を伴う。
区別: Issue #447はenvバックドシークレットを持つ `health` コマンドを対象とする; このissueはファイルバックドシークレットを持つ `status` を対象とする。
共通の原因: コマンドレベルの設定解決前の過早なプラグイン読み込み。 - Issue #389: `ensurePluginRegistryLoaded()` が渡された設定パラメータを無視する
症状の重複: 関数は設定の上書きを受け入れるが、使用しない。
区別: Issue #389はAPIデザインのバグです; このissueはユーザー向けの失敗の結果です。
共通の修正: `options?.config ?? loadConfig()` への同じ1行の変更。 - Issue #412: `loadPlugins` コールバックがコマンドハンドラコンテキスト可以利用前に評価される
症状の重複: ルートレベルのプラグインプリロードが解決済み設定にアクセスできない。
区別: Issue #412はアーキテクチャ分析です; このissueはSecretRefでの具体的な再現です。
共通の修正: 解決済み設定が必要なコマンドのルートレベルプラグインプリロードを無効化。 - Issue #523: バックスラッシュパスでWindowsでファイルバックドSecretRefプロバイダーが例外をスローする
症状の重複: `status` コマンドでのファイルバックドSecretRefの使用。
区別: Issue #523はパス区切り文字の正規化です; このissueはライフサイクル順序です。
潜在的な相互作用: Windowsユーザーは両issue同時に遭遇する可能性があります。
関連する設定パターン
- 環境変数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"}`
デバッグコマンド
# Enable verbose plugin loading
OPENCLAW_DEBUG=plugin-load openclaw status
# Enable config resolution tracing
OPENCLAW_DEBUG=config-resolution openclaw status
# Check loaded plugins
openclaw plugin list
# Verify SecretRef resolution chain
openclaw secret-refs --trace channels.feishu.appSecret
# Dump raw vs resolved config
openclaw config --raw | jq '.channels.feishu.appSecret'
openclaw config --resolved | jq '.channels.feishu.appSecret'