Relationships
#3246 Extension auto-resolution installs an unrelated extension when a registry search guess does not provide the type
Opened by stack72 · 10/11/2026· Shipped 10/11/2026
What happened
Auto-resolution of a type that no extension publishes installs an unrelated extension picked by a fuzzy registry search, writes it into the tracked lockfile, and reports success:
$ swamp model create @swamp/definitely-not-a-real-type-3243 probe
Resolving @swamp/definitely-not-a-real-type-3243 not found locally, searching registry...
Installing @swamp/gcp/realtimebidding@2026.09.29.1 (Google Cloud realtimebidding infrastructure models)
Installed @swamp/gcp/realtimebidding@2026.09.29.1 (8 models registered)
Error: Unknown model type: @swamp/definitely-not-a-real-type-3243. Available types: command/shell, @swamp/gcp/realtimebidding/bidders, ...After this, .swamp/pulled-extensions/@swamp/gcp/realtimebidding/ exists and extensions/models/upstream_extensions.json carries an entry for it, so the unrelated extension lands in the user's next commit. A second run reports @swamp/gcp/realtimebidding@2026.09.29.1 is installed but does not provide @swamp/definitely-not-a-real-type-3243, which is accurate, but the extension stays installed.
Realistic typos in the default-trusted @swamp collective hit the same path. The search returns:
| Type typed | Extension that would be installed |
|---|---|
@swamp/aws/ecc2/instance |
@swamp/aws/workspacesinstances |
@swamp/gcp/computee/instance |
@swamp/aws/workspacesinstances (AWS for a GCP type) |
Reproduced with the released binary (20261011.001259.0) in a fresh swamp repo init --tool none repo against swamp-club.com.
Cause
- The search fallback takes the first hit unverified.
findExtensionForType(src/domain/extensions/extension_auto_resolver.ts~609) tries direct name candidates (the type and its parent prefixes), then callssearchExtensions({ q, collective, perPage: 1 })with the type's segments as words and returns the first result. Nothing checks that the hit provides the type. The made-up type above matched on the word "real". - A fresh install never checks the type was provided.
installAndLoadLocked(~495) printsinstalledand returns true afterextensionInstaller.install(). The already-installed (intact) branch does checkextensionInstaller.providesType(normalizedType)and reportsinstalledWithoutType/alreadyInstalledButFailed; the fresh-install branch does not.
findExtensionForType is shared, so the unverified guess also reaches describeTypeInRegistry (harmless: config_fields.ts matches the exact type in the returned metadata) and datastoreExtensionCandidates (the guessed extension joins the set exempted to write the in-repo lockfile during datastore repair).
The search fallback is load-bearing
A scan of all 1,709 registry extensions (4,611 published types) found 200 types reachable only through search, because the extension name is not a prefix of the type, e.g. @shelson/newrelic publishes @shelson/newrelic-account, @dougschaefer/pexip-infinity publishes @dougschaefer/pexip-conference. None are in @swamp; all are community collectives. So the fallback cannot simply be removed.
For those 200, the first search hit is wrong 4 times today:
| Type | Provided by | Search's first hit (installed today) |
|---|---|---|
@dougschaefer/azure-firewall |
@dougschaefer/azure |
@dougschaefer/opnsense-firewall |
@keeb/tmdb-lookup |
@keeb/mms |
@keeb/twilio/lookups |
@rjeschmi/rancher-openstack |
@rjeschmi/vm |
@rjeschmi/kolla-openstack |
@rjeschmi/vm-provider-openstack |
@rjeschmi/vm |
@rjeschmi/kolla-openstack |
Expected
An extension is installed by auto-resolution only when it is known to provide the requested type. Otherwise the user sees no extension publishes this type, and nothing is installed or written to the lockfile.
Proposed fix
- Verify search hits against registry metadata in
findExtensionForType. Each version's published detail (GET /api/v1/extensions/<name>/latest,latestVersionDetail.models|vaults|datastores|drivers[].type, already exposed asExtensionLookupPort.getLatestVersionDetail) lists every type it provides. Request several search results instead of one and return the first whose metadata lists the type (case-insensitive, asconfig_fields.tsdoes). This fixes made-up types, typos and the 4 wrong picks above, and the shared callers inherit it. If the lookup port cannot fetch detail, keep today's behaviour only behind the post-install check below. - Post-install backstop. In the fresh-install branch of
installAndLoadLocked, run the sameprovidesTypecheck as theintactbranch and reportinstalledWithoutTypeinstead ofinstalledwhen the type is absent. - No verified match: report
notFound, optionally with a hint naming the closest search hit andswamp extension pull <name>, without installing it.
No interactive prompt: a verified match is the right extension, and an unverified guess is better offered as a hint than a prompt that cannot run in CI, --json, serve or workers.
Not a regression: the unverified search fallback has existed since auto-resolution shipped (#725).
Tests
- Unit:
findExtensionForTypeskips a search hit whose metadata lacks the type and returns a later hit that lists it; returns null when none do; direct candidates unchanged. - Unit: fresh install of an extension without the type reports
installedWithoutType, notinstalled. - Integration: resolving a made-up type against a lookup whose search returns an unrelated extension installs nothing and writes no lockfile entry.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.