Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneeszack

Relationships

#3365 vault type search shows an extension vault type under its type id with no description, and cannot find it by name or description

Opened by zack · 10/13/2026· Shipped 10/13/2026

Severity: low — a discovery command shows wrong display metadata and returns an empty result (exit 0) for queries that should match. The type id is still listed and searchable, vault create works, and no user data or state is affected.

Affects: found on swamp 20261009.215038.0-sha.986f9022; both UAT tests below still fail the same way on 20261013.025835.0-sha.1b6f0c59 (2026-10-13). The placeholder synthesis and the filter are unchanged on swamp main at 976882d7. The hand reproduction below was taken at 986f9022 and not repeated on the newer build.

Summary

A user-defined vault type declares type, name and description. After swamp indexes the extension, swamp vault type search handles it wrongly in two ways that share one cause:

  1. Listing. The type is listed with name equal to the type id and description empty.
  2. Query. swamp vault type search <query> is documented and implemented as a case-insensitive match on type id, name or description. For an extension type only the type id matches; a query on the declared name or description returns results: []. The same query forms work for the built-in local_encryption.

The listing also depends on load history. The one run directly after the source file is edited shows the declared name and description. Every later run shows the type id as the name again, now with the description filled. A successful vault create of that type, which reports the declared name as typeName, does not change the listing.

Steps to reproduce

cd "$(mktemp -d)" && swamp init
mkdir -p extensions/vaults/test
cat > extensions/vaults/test/mock-vault.ts <<'EOF'
import { z } from "npm:zod@4";

export const vault = {
  type: "@test/mock-vault",
  name: "Mock Vault",
  description:
    "File-backed mock vault for testing user-defined vault extensions.",
  configSchema: z.object({ endpoint: z.string() }),
  createProvider(name: string, _config: { endpoint: string }) {
    const store: Record<string, string> = {};
    return {
      getName: () => name,
      get: (key: string) => Promise.resolve(store[key]),
      put: (key: string, value: string) => {
        store[key] = value;
        return Promise.resolve();
      },
      list: () => Promise.resolve(Object.keys(store)),
    };
  },
};
EOF

swamp vault type search --json                 # 1: name "@test/mock-vault", description ""
swamp vault type search "mock vault" --json    # 2: the declared name       -> results: []
swamp vault type search file-backed --json     # 3: a description-only word -> results: []
swamp vault type search mock-vault --json      # 4: the type id             -> matches
swamp vault create @test/mock-vault mv --config '{"endpoint":"x"}' --json   # 5: typeName "Mock Vault"
swamp vault type search --json                 # 6: unchanged from 1

echo '// edit' >> extensions/vaults/test/mock-vault.ts
swamp vault type search --json                 # 7: name "Mock Vault", description filled
swamp vault type search --json                 # 8: name "@test/mock-vault" again, description filled

# The built-in type matches on name and description:
swamp vault type search "local encryption" --json
swamp vault type search aes-gcm --json

Expected behaviour

Every run lists the type as the extension declares it:

{ "type": "@test/mock-vault", "name": "Mock Vault", "description": "File-backed mock vault for testing user-defined vault extensions." }

and swamp vault type search "mock vault" --json and swamp vault type search file-backed --json each return that one type, the way the equivalent queries return local_encryption.

What establishes this:

  • .claude/skills/swamp/references/extension/references/vault/api.md:86-93: name is the "Human-readable display name shown in CLI output and status commands"; description is "Shown in extension listings and search results".
  • .claude/skills/swamp/references/vault/references/troubleshooting.md:155-156 tells users to run swamp vault type search --json to "View available types and their descriptions".
  • src/presentation/renderers/vault_type_search.tsx:34-50: the filter is documented as "case-insensitive match on type, name, or description" and is implemented that way.
  • design/primitives/vaults.md:342: swamp vault type-search [keyword] is to "browse registered vault types (built-in and extension)". No distinction is drawn between the two for keyword matching.
  • src/domain/vaults/vault_types_test.ts:68-77 ("all vault types have required fields") asserts description.length > 0 for everything getVaultTypes() returns.
  • swamp vault create @test/mock-vault ... --json reports "typeName": "Mock Vault" for the same extension, so the declared name is what the rest of the CLI shows.

design/primitives/extensions.md:2851-2852 says type search imports no bundles. That is compatible with the expectation: it constrains how the listing is produced, not what it shows.

Actual behaviour

All commands exit 0.

$ swamp vault type search --json
{
  "query": "",
  "results": [
    {
      "type": "local_encryption",
      "name": "Local Encryption",
      "description": "Store encrypted secrets in local files using AES-GCM encryption. Uses SSH private key or auto-generated key for encryption."
    },
    {
      "type": "@test/mock-vault",
      "name": "@test/mock-vault",
      "description": ""
    }
  ]
}
$ swamp vault type search "mock vault" --json
{
  "query": "mock vault",
  "results": []
}
$ swamp vault type search file-backed --json
{
  "query": "file-backed",
  "results": []
}
$ swamp vault type search mock-vault --json
{
  "query": "mock-vault",
  "results": [
    {
      "type": "@test/mock-vault",
      "name": "@test/mock-vault",
      "description": ""
    }
  ]
}

The two failing queries are still empty after vault create has imported the bundle in an earlier process.

The listing across load history, same extension, all exit 0:

fresh index:       {'type': '@test/mock-vault', 'name': '@test/mock-vault', 'description': ''}
second run:        {'type': '@test/mock-vault', 'name': '@test/mock-vault', 'description': ''}
run after edit:    {'type': '@test/mock-vault', 'name': 'Mock Vault', 'description': 'File-backed mock vault for testing user-defined vault extensions.'}
next run:          {'type': '@test/mock-vault', 'name': '@test/mock-vault', 'description': 'File-backed mock vault for testing user-defined vault extensions.'}
run after that:    {'type': '@test/mock-vault', 'name': '@test/mock-vault', 'description': 'File-backed mock vault for testing user-defined vault extensions.'}

After the source edit, file-backed starts matching (the catalog description is now filled), but mock vault still returns [] except in the single run that imports the bundle.

After the fresh index, sqlite3 .swamp/_extension_catalog.db "select type_normalized,description from bundle_types" prints @test/mock-vault| (empty description).

Root cause

Paths at 986f9022.

  • src/domain/vaults/vault_types.ts:65-79: getVaultTypes() synthesises every lazy (indexed, not imported) type as name: entry.type (line 73) and description: entry.description ?? "" (line 74). LazyVaultEntry (src/domain/vaults/vault_type_registry.ts:48-54) has no name field and the catalog has no name column, so a lazy type can never show its declared name.
  • src/domain/extensions/extension_loader.ts:1437-1447: the index-only path (bundle without importing) writes the catalog row with description: "". Line 1665 does the same. Only the import path at :1585-1591 writes the declared description. vault_kind_adapter.ts:75-91 (extractTypeFromSource) extracts only the type id from source.
  • src/cli/commands/vault_type_search.ts:80-84 calls vaultTypeRegistry.ensureLoaded() and then getVaultTypes() without importing the lazily indexed types, and src/libswamp/vaults/type_search.ts:83-87 passes the values straight to the output.
  • src/presentation/renderers/vault_type_search.tsx:38-50 then filters on those placeholder values.

So a first run indexes with an empty description. A source edit makes the next run import the bundle (declared name from the loaded registry, description written to the catalog). Later runs are lazy again, with the type id as the name and the description now read from the catalog.

The lazy synthesis was introduced deliberately by 8f12f228 ("feat: lazy per-bundle loading for vault, driver, datastore, and report registries (#1089)", 2026-04-06) for start-up time. That commit did not carry the name, or reliably the description, into the catalog.

Fix direction

One fix covers both symptoms. Two routes exist:

  • Carry the declared name and description in the catalog at index time. This keeps type search from importing bundles, as the extensions design says.
  • Force-load the lazy entries before listing. The sibling swamp report type search had the same symptom and was changed this way in 2a5c3529 (#1564), src/cli/commands/report_type_search.ts:81-85. This goes against the "imports no bundles" sentence for vault type search.

src/domain/datastore/datastore_types.ts:51-65 builds its lazy entries the same way (name: entry.type, description: ""), so swamp datastore type search is likely to have the same symptom. That was not reproduced.

How the UAT found it

Test file tests/cli/vault/type/search_test.ts in swamp-uat (branch test-investigation, not yet on swamp-uat main), two tests:

  • "swamp vault type search --json lists a user-defined vault type under its declared name and description"
  • "swamp vault type search finds a user-defined vault type by its declared name and description"

Before the audit the file had no test with an extension vault type and none that passed a query. Its tests ran vault type search in a new repo, where only local_encryption exists, and checked that results was non-empty, that local_encryption was present, and that each type/name/description was a string. A placeholder name and an empty description satisfied all of that.

The first test writes the fixture extension (tests/cli/fixtures/extensions/vaults/test/mock-vault.ts), runs swamp vault type search --json twice, and asserts the @test/mock-vault entry equals exactly the declared type, name and description on both runs. It fails on the first run:

AssertionError: Values are not equal: first run
-     description: "",
-     name: "@test/mock-vault",
+     description: "File-backed mock vault for testing user-defined vault extensions.",
+     name: "Mock Vault",
      type: "@test/mock-vault",

The second test asserts:

assertEquals(await types("mock vault"), ["@test/mock-vault"]);   // the declared name
assertEquals(await types("file-backed"), ["@test/mock-vault"]);  // a description-only word

It fails on the first assertion with actual [].

Run them from the swamp-uat checkout:

deno test --allow-read --allow-write --allow-env --allow-run --allow-net \
  tests/cli/vault/type/search_test.ts \
  --filter "user-defined vault type"
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 3 MOREFINDINGS+ 17 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/13/2026, 7:25:42 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/13/2026, 3:40:48 PM

Sign in to post a ripple.