From 0c2f5d7bc9ce72f6e19fdf2edce8b7737ca4ef96 Mon Sep 17 00:00:00 2001 From: clawbot Date: Sun, 15 Feb 2026 14:04:37 -0800 Subject: [PATCH] Skip unlocker directories with missing metadata instead of failing When an unlocker directory exists but is missing unlocker-metadata.json, log a debug warning and skip it instead of returning a hard error that crashes the entire 'unlocker ls' command. Closes #1 --- internal/vault/unlockers.go | 4 ++- internal/vault/vault_test.go | 54 ++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/internal/vault/unlockers.go b/internal/vault/unlockers.go index b7a2087..c0f501a 100644 --- a/internal/vault/unlockers.go +++ b/internal/vault/unlockers.go @@ -213,7 +213,9 @@ func (v *Vault) ListUnlockers() ([]UnlockerMetadata, error) { return nil, fmt.Errorf("failed to check if metadata exists for unlocker %s: %w", file.Name(), err) } if !exists { - return nil, fmt.Errorf("unlocker directory %s is missing metadata file", file.Name()) + secret.Debug("Skipping unlocker directory with missing metadata file", "directory", file.Name()) + + continue } metadataBytes, err := afero.ReadFile(v.fs, metadataPath) diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go index a69bbdf..15ac662 100644 --- a/internal/vault/vault_test.go +++ b/internal/vault/vault_test.go @@ -243,3 +243,57 @@ func TestVaultOperations(t *testing.T) { } }) } + +func TestListUnlockers_SkipsMissingMetadata(t *testing.T) { + // Set test environment variables + testMnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + t.Setenv(secret.EnvMnemonic, testMnemonic) + t.Setenv(secret.EnvUnlockPassphrase, "test-passphrase") + + // Use in-memory filesystem + fs := afero.NewMemMapFs() + stateDir := "/test/state" + + // Create vault + vlt, err := CreateVault(fs, stateDir, "test-vault") + if err != nil { + t.Fatalf("Failed to create vault: %v", err) + } + + // Create a passphrase unlocker so we have at least one valid unlocker + passphraseBuffer := memguard.NewBufferFromBytes([]byte("test-passphrase")) + defer passphraseBuffer.Destroy() + _, err = vlt.CreatePassphraseUnlocker(passphraseBuffer) + if err != nil { + t.Fatalf("Failed to create passphrase unlocker: %v", err) + } + + // Create a bogus unlocker directory with no metadata file + vaultDir, err := vlt.GetDirectory() + if err != nil { + t.Fatalf("Failed to get vault directory: %v", err) + } + bogusDir := filepath.Join(vaultDir, "unlockers.d", "bogus-no-metadata") + err = fs.MkdirAll(bogusDir, 0o700) + if err != nil { + t.Fatalf("Failed to create bogus directory: %v", err) + } + + // ListUnlockers should succeed, skipping the bogus directory + unlockers, err := vlt.ListUnlockers() + if err != nil { + t.Fatalf("ListUnlockers returned error when it should have skipped bad directory: %v", err) + } + + // Should still have the valid passphrase unlocker + if len(unlockers) == 0 { + t.Errorf("Expected at least one unlocker, got none") + } + + // Verify we only got the valid unlocker(s), not the bogus one + for _, u := range unlockers { + if u.Type == "" { + t.Errorf("Got unlocker with empty type, likely from bogus directory") + } + } +}