Files
continue/core/config/yaml/loadYaml.ts
shanevcantwell 38aae62e56 fix: pass pre-read content to RegistryClient for WSL compatibility (#9739)
Fixes #6242, #7810

When Windows VS Code connects to WSL, workspace files are accessed via
vscode-remote:// URIs. Previously, getAllDotContinueDefinitionFiles()
correctly read content via ide.readFile(), but loadYaml.ts discarded
the content and passed only the URI path to RegistryClient, which then
tried to re-read using fs.readFileSync() - failing for remote URIs.

Changes:
- Add optional content field to FileIdentifier (non-breaking)
- RegistryClient.getContent() returns pre-read content when present
- loadYaml.ts passes content along with file identifiers

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 15:19:25 -08:00

476 lines
14 KiB
TypeScript

import {
AssistantUnrolled,
BLOCK_TYPES,
ConfigResult,
ConfigValidationError,
isAssistantUnrolledNonNullable,
mergeConfigYamlRequestOptions,
mergeUnrolledAssistants,
ModelRole,
PackageIdentifier,
RegistryClient,
unrollAssistant,
validateConfigYaml,
} from "@continuedev/config-yaml";
import { dirname } from "node:path";
import {
ContinueConfig,
IDE,
IdeInfo,
IdeSettings,
ILLMLogger,
InternalMcpOptions,
} from "../..";
import { MCPManagerSingleton } from "../../context/mcp/MCPManagerSingleton";
import { ControlPlaneClient } from "../../control-plane/client";
import TransformersJsEmbeddingsProvider from "../../llm/llms/TransformersJsEmbeddingsProvider";
import { getAllPromptFiles } from "../../promptFiles/getPromptFiles";
import { GlobalContext } from "../../util/GlobalContext";
import { modifyAnyConfigWithSharedConfig } from "../sharedConfig";
import { convertPromptBlockToSlashCommand } from "../../commands/slash/promptBlockSlashCommand";
import { slashCommandFromPromptFile } from "../../commands/slash/promptFileSlashCommand";
import { loadJsonMcpConfigs } from "../../context/mcp/json/loadJsonMcpConfigs";
import { getControlPlaneEnvSync } from "../../control-plane/env";
import { PolicySingleton } from "../../control-plane/PolicySingleton";
import { getBaseToolDefinitions } from "../../tools";
import { getCleanUriPath } from "../../util/uri";
import { loadConfigContextProviders } from "../loadContextProviders";
import { getAllDotContinueDefinitionFiles } from "../loadLocalAssistants";
import { unrollLocalYamlBlocks } from "./loadLocalYamlBlocks";
import { LocalPlatformClient } from "./LocalPlatformClient";
import { llmsFromModelConfig } from "./models";
import {
convertYamlMcpConfigToInternalMcpOptions,
convertYamlRuleToContinueRule,
} from "./yamlToContinueConfig";
async function loadConfigYaml(options: {
overrideConfigYaml: AssistantUnrolled | undefined;
controlPlaneClient: ControlPlaneClient;
orgScopeId: string | null;
ideSettings: IdeSettings;
ide: IDE;
packageIdentifier: PackageIdentifier;
}): Promise<ConfigResult<AssistantUnrolled>> {
const {
overrideConfigYaml,
controlPlaneClient,
orgScopeId,
ideSettings,
ide,
packageIdentifier,
} = options;
// Add local .continue blocks
// Use "content" field to pass pre-read content directly, avoiding
// fs.readFileSync which fails for vscode-remote:// URIs in WSL (#6242, #7810)
const localBlockPromises = BLOCK_TYPES.map(async (blockType) => {
const localBlocks = await getAllDotContinueDefinitionFiles(
ide,
{ includeGlobal: true, includeWorkspace: true, fileExtType: "yaml" },
blockType,
);
return localBlocks.map((b) => ({
uriType: "file" as const,
fileUri: b.path,
content: b.content,
}));
});
const localPackageIdentifiers: PackageIdentifier[] = (
await Promise.all(localBlockPromises)
).flat();
// logger.info(
// `Loading config.yaml from ${JSON.stringify(packageIdentifier)} with root path ${rootPath}`,
// );
// Registry client is only used if local blocks are present, but logic same for hub/local assistants
const getRegistryClient = async () => {
const rootPath =
packageIdentifier.uriType === "file"
? dirname(getCleanUriPath(packageIdentifier.fileUri))
: undefined;
return new RegistryClient({
accessToken: await controlPlaneClient.getAccessToken(),
apiBase: getControlPlaneEnvSync(ideSettings.continueTestEnvironment)
.CONTROL_PLANE_URL,
rootPath,
});
};
const errors: ConfigValidationError[] = [];
let config: AssistantUnrolled | undefined;
if (overrideConfigYaml) {
config = overrideConfigYaml;
if (localPackageIdentifiers.length > 0) {
const unrolledLocal = await unrollLocalYamlBlocks(
localPackageIdentifiers,
ide,
await getRegistryClient(),
orgScopeId,
controlPlaneClient,
);
if (unrolledLocal.errors) {
errors.push(...unrolledLocal.errors);
}
if (unrolledLocal.config) {
config = mergeUnrolledAssistants(config, unrolledLocal.config);
}
}
} else {
// This is how we allow use of blocks locally
const unrollResult = await unrollAssistant(
packageIdentifier,
await getRegistryClient(),
{
renderSecrets: true,
currentUserSlug: "",
onPremProxyUrl: null,
orgScopeId,
platformClient: new LocalPlatformClient(
orgScopeId,
controlPlaneClient,
ide,
),
injectBlocks: localPackageIdentifiers,
},
);
config = unrollResult.config;
if (unrollResult.errors) {
errors.push(...unrollResult.errors);
}
}
if (config && isAssistantUnrolledNonNullable(config)) {
errors.push(...validateConfigYaml(config));
}
if (errors?.some((error) => error.fatal)) {
return {
errors,
config: undefined,
configLoadInterrupted: true,
};
}
// Set defaults if undefined (this lets us keep config.json uncluttered for new users)
return {
config,
errors,
configLoadInterrupted: false,
};
}
export async function configYamlToContinueConfig(options: {
config: AssistantUnrolled;
ide: IDE;
ideInfo: IdeInfo;
uniqueId: string;
llmLogger: ILLMLogger;
workOsAccessToken: string | undefined;
}): Promise<{ config: ContinueConfig; errors: ConfigValidationError[] }> {
let { config, ide, ideInfo, uniqueId, llmLogger } = options;
const localErrors: ConfigValidationError[] = [];
const continueConfig: ContinueConfig = {
slashCommands: [],
tools: getBaseToolDefinitions(),
mcpServerStatuses: [],
contextProviders: [],
modelsByRole: {
chat: [],
edit: [],
apply: [],
embed: [],
autocomplete: [],
rerank: [],
summarize: [],
subagent: [],
},
selectedModelByRole: {
chat: null,
edit: null, // not currently used
apply: null,
embed: null,
autocomplete: null,
rerank: null,
summarize: null,
subagent: null,
},
rules: [],
requestOptions: { ...config.requestOptions },
};
// Right now, if there are any missing packages in the config, then we will just throw an error
if (!isAssistantUnrolledNonNullable(config)) {
return {
config: continueConfig,
errors: [
{
message:
"Failed to load config due to missing blocks, see which blocks are missing below",
fatal: true,
},
],
};
}
for (const rule of config.rules ?? []) {
const convertedRule = convertYamlRuleToContinueRule(rule);
continueConfig.rules.push(convertedRule);
}
continueConfig.data = config.data?.map((d) => ({
...d,
requestOptions: mergeConfigYamlRequestOptions(
d.requestOptions,
continueConfig.requestOptions,
),
}));
continueConfig.docs = config.docs?.map((doc) => ({
title: doc.name,
startUrl: doc.startUrl,
rootUrl: doc.rootUrl,
faviconUrl: doc.faviconUrl,
useLocalCrawling: doc.useLocalCrawling,
sourceFile: doc.sourceFile,
}));
// Prompt files -
try {
const promptFiles = await getAllPromptFiles(ide, undefined, true);
promptFiles.forEach((file) => {
try {
const slashCommand = slashCommandFromPromptFile(
file.path,
file.content,
);
if (slashCommand) {
continueConfig.slashCommands?.push(slashCommand);
}
} catch (e) {
localErrors.push({
fatal: false,
message: `Failed to convert prompt file ${file.path} to slash command: ${e instanceof Error ? e.message : e}`,
});
}
});
} catch (e) {
localErrors.push({
fatal: false,
message: `Error loading local prompt files: ${e instanceof Error ? e.message : e}`,
});
}
config.prompts?.forEach((prompt) => {
try {
const slashCommand = convertPromptBlockToSlashCommand(prompt);
continueConfig.slashCommands?.push(slashCommand);
} catch (e) {
localErrors.push({
message: `Error loading prompt ${prompt.name}: ${e instanceof Error ? e.message : e}`,
fatal: false,
});
}
});
// Models
let warnAboutFreeTrial = false;
const defaultModelRoles: ModelRole[] = ["chat", "summarize", "apply", "edit"];
for (const model of config.models ?? []) {
model.roles = model.roles ?? defaultModelRoles; // Default to all 4 chat-esque roles if not specified
if (model.provider === "free-trial") {
warnAboutFreeTrial = true;
}
try {
const llms = await llmsFromModelConfig({
model,
uniqueId,
llmLogger,
config: continueConfig,
});
if (model.roles?.includes("chat")) {
continueConfig.modelsByRole.chat.push(...llms);
}
if (model.roles?.includes("summarize")) {
continueConfig.modelsByRole.summarize.push(...llms);
}
if (model.roles?.includes("apply")) {
continueConfig.modelsByRole.apply.push(...llms);
}
if (model.roles?.includes("edit")) {
continueConfig.modelsByRole.edit.push(...llms);
}
if (model.roles?.includes("autocomplete")) {
continueConfig.modelsByRole.autocomplete.push(...llms);
}
if (model.roles?.includes("embed")) {
const { provider } = model;
if (provider === "transformers.js") {
if (ideInfo.ideType === "vscode") {
continueConfig.modelsByRole.embed.push(
new TransformersJsEmbeddingsProvider(),
);
} else {
localErrors.push({
fatal: false,
message: `Transformers.js embeddings provider not supported in this IDE.`,
});
}
} else {
continueConfig.modelsByRole.embed.push(...llms);
}
}
if (model.roles?.includes("rerank")) {
continueConfig.modelsByRole.rerank.push(...llms);
}
if (model.roles?.includes("subagent")) {
continueConfig.modelsByRole.subagent.push(...llms);
}
} catch (e) {
localErrors.push({
fatal: false,
message: `Failed to load model:\nName: ${model.name}\nModel: ${model.model}\nProvider: ${model.provider}\n${e instanceof Error ? e.message : e}`,
});
}
}
// Add transformers js to the embed models in vs code if not already added
if (
ideInfo.ideType === "vscode" &&
!continueConfig.modelsByRole.embed.find(
(m) => m.providerName === "transformers.js",
)
) {
continueConfig.modelsByRole.embed.push(
new TransformersJsEmbeddingsProvider(),
);
}
if (warnAboutFreeTrial) {
localErrors.push({
fatal: false,
message:
"Model provider 'free-trial' is no longer supported, will be ignored.",
});
}
const { providers, errors: contextErrors } = loadConfigContextProviders(
config.context,
!!config.docs?.length,
ideInfo.ideType,
);
continueConfig.contextProviders = providers;
localErrors.push(...contextErrors);
// Trigger MCP server refreshes (Config is reloaded again once connected!)
const mcpManager = MCPManagerSingleton.getInstance();
const orgPolicy = PolicySingleton.getInstance().policy;
if (orgPolicy?.policy?.allowMcpServers === false) {
await mcpManager.shutdown();
} else {
const mcpOptions: InternalMcpOptions[] = (config.mcpServers ?? []).map(
(server) =>
convertYamlMcpConfigToInternalMcpOptions(server, config.requestOptions),
);
const { errors: jsonMcpErrors, mcpServers } = await loadJsonMcpConfigs(
ide,
true,
config.requestOptions,
);
localErrors.push(...jsonMcpErrors);
mcpOptions.push(...mcpServers);
mcpManager.setConnections(mcpOptions, false, { ide });
}
return { config: continueConfig, errors: localErrors };
}
export async function loadContinueConfigFromYaml(options: {
ide: IDE;
ideSettings: IdeSettings;
ideInfo: IdeInfo;
uniqueId: string;
llmLogger: ILLMLogger;
workOsAccessToken: string | undefined;
overrideConfigYaml: AssistantUnrolled | undefined;
controlPlaneClient: ControlPlaneClient;
orgScopeId: string | null;
packageIdentifier: PackageIdentifier;
}): Promise<ConfigResult<ContinueConfig>> {
const {
ide,
ideSettings,
ideInfo,
uniqueId,
llmLogger,
workOsAccessToken,
overrideConfigYaml,
controlPlaneClient,
orgScopeId,
packageIdentifier,
} = options;
const configYamlResult = await loadConfigYaml({
overrideConfigYaml,
controlPlaneClient,
orgScopeId,
ideSettings,
ide,
packageIdentifier,
});
if (!configYamlResult.config || configYamlResult.configLoadInterrupted) {
return {
errors: configYamlResult.errors,
config: undefined,
configLoadInterrupted: true,
};
}
const { config: continueConfig, errors: localErrors } =
await configYamlToContinueConfig({
config: configYamlResult.config,
ide,
ideInfo,
uniqueId,
llmLogger,
workOsAccessToken,
});
// Apply shared config
// TODO: override several of these values with user/org shared config
// Don't try catch this - has security implications and failure should be fatal
const sharedConfig = new GlobalContext().getSharedConfig();
const withShared = modifyAnyConfigWithSharedConfig(
continueConfig,
sharedConfig,
);
if (withShared.allowAnonymousTelemetry === undefined) {
withShared.allowAnonymousTelemetry = true;
}
return {
config: withShared,
errors: [...(configYamlResult.errors ?? []), ...localErrors],
configLoadInterrupted: false,
};
}