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:
- Listing. The type is listed with
nameequal to the type id anddescriptionempty. - 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 returnsresults: []. The same query forms work for the built-inlocal_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 --jsonExpected 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:nameis the "Human-readable display name shown in CLI output and status commands";descriptionis "Shown in extension listings and search results"..claude/skills/swamp/references/vault/references/troubleshooting.md:155-156tells users to runswamp vault type search --jsonto "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") assertsdescription.length > 0for everythinggetVaultTypes()returns.swamp vault create @test/mock-vault ... --jsonreports"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 asname: entry.type(line 73) anddescription: entry.description ?? ""(line 74).LazyVaultEntry(src/domain/vaults/vault_type_registry.ts:48-54) has nonamefield 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 withdescription: "". Line 1665 does the same. Only the import path at:1585-1591writes 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-84callsvaultTypeRegistry.ensureLoaded()and thengetVaultTypes()without importing the lazily indexed types, andsrc/libswamp/vaults/type_search.ts:83-87passes the values straight to the output.src/presentation/renderers/vault_type_search.tsx:38-50then 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 searchhad 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 wordIt 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"Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.