* Shared discovery/policy/merge pipeline for both load modes. * * Resolves enabledPlugins → marketplace entries, runs enterprise policy * checks, pre-loads catalogs, then dispatches each entry to the full or * cache-only per-entry loader. The ONLY difference between loadAllPlugins * and loadAll
({
cacheOnly,
}: {
cacheOnly: boolean
})
| 1886 | * are identical. |
| 1887 | */ |
| 1888 | async function loadPluginsFromMarketplaces({ |
| 1889 | cacheOnly, |
| 1890 | }: { |
| 1891 | cacheOnly: boolean |
| 1892 | }): Promise<{ |
| 1893 | plugins: LoadedPlugin[] |
| 1894 | errors: PluginError[] |
| 1895 | }> { |
| 1896 | const settings = getSettings_DEPRECATED() |
| 1897 | // Merge --add-dir plugins at lowest priority; standard settings win on conflict |
| 1898 | const enabledPlugins = { |
| 1899 | ...getAddDirEnabledPlugins(), |
| 1900 | ...(settings.enabledPlugins || {}), |
| 1901 | } |
| 1902 | const plugins: LoadedPlugin[] = [] |
| 1903 | const errors: PluginError[] = [] |
| 1904 | |
| 1905 | // Filter to plugin@marketplace format and validate |
| 1906 | const marketplacePluginEntries = Object.entries(enabledPlugins).filter( |
| 1907 | ([key, value]) => { |
| 1908 | // Check if it's in plugin@marketplace format (includes both enabled and disabled) |
| 1909 | const isValidFormat = PluginIdSchema().safeParse(key).success |
| 1910 | if (!isValidFormat || value === undefined) return false |
| 1911 | // Skip built-in plugins — handled separately by getBuiltinPlugins() |
| 1912 | const { marketplace } = parsePluginIdentifier(key) |
| 1913 | return marketplace !== BUILTIN_MARKETPLACE_NAME |
| 1914 | }, |
| 1915 | ) |
| 1916 | |
| 1917 | // Load known marketplaces config to look up sources for policy checking. |
| 1918 | // Use the Safe variant so a corrupted config file doesn't crash all plugin |
| 1919 | // loading — this is a read-only path, so returning {} degrades gracefully. |
| 1920 | const knownMarketplaces = await loadKnownMarketplacesConfigSafe() |
| 1921 | |
| 1922 | // Fail-closed guard for enterprise policy: if a policy IS configured and we |
| 1923 | // cannot resolve a marketplace's source (config returned {} due to corruption, |
| 1924 | // or entry missing), we must NOT silently skip the policy check and load the |
| 1925 | // plugin anyway. Before Safe, a corrupted config crashed everything (loud, |
| 1926 | // fail-closed). With Safe + no guard, the policy check short-circuits on |
| 1927 | // undefined marketplaceConfig and the fallback path (getPluginByIdCacheOnly) |
| 1928 | // loads the plugin unchecked — a silent fail-open. This guard restores |
| 1929 | // fail-closed: unknown source + active policy → block. |
| 1930 | // |
| 1931 | // Allowlist: any value (including []) is active — empty allowlist = deny all. |
| 1932 | // Blocklist: empty [] is a semantic no-op — only non-empty counts as active. |
| 1933 | const strictAllowlist = getStrictKnownMarketplaces() |
| 1934 | const blocklist = getBlockedMarketplaces() |
| 1935 | const hasEnterprisePolicy = |
| 1936 | strictAllowlist !== null || (blocklist !== null && blocklist.length > 0) |
| 1937 | |
| 1938 | // Pre-load marketplace catalogs once per marketplace rather than re-reading |
| 1939 | // known_marketplaces.json + marketplace.json for every plugin. This is the |
| 1940 | // hot path — with N plugins across M marketplaces, the old per-plugin |
| 1941 | // getPluginByIdCacheOnly() did 2N config reads + N catalog reads; this does M. |
| 1942 | const uniqueMarketplaces = new Set( |
| 1943 | marketplacePluginEntries |
| 1944 | .map(([pluginId]) => parsePluginIdentifier(pluginId).marketplace) |
| 1945 | .filter((m): m is string => !!m), |
no test coverage detected