From 70310a37b323f1c55a73ed3391dd956f1e0ae0ce Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 26 Sep 2025 03:23:25 -0400 Subject: [PATCH] validate config directory --- packages/opencode/src/config/config.ts | 239 ++++++++++++++----------- 1 file changed, 130 insertions(+), 109 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 84ee9a16..c25c6ea0 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -49,6 +49,8 @@ export namespace Config { } result.agent = result.agent || {} + result.mode = result.mode || {} + result.plugin = result.plugin || [] const directories = [ Global.Path.config, @@ -57,109 +59,14 @@ export namespace Config { )), ] - const markdownAgents = [ - ...(await Filesystem.globUp("agent/**/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/agent/**/*.md", Instance.directory, Instance.worktree)), - ] - for (const item of markdownAgents) { - const content = await Bun.file(item).text() - const md = matter(content) - if (!md.data) continue - - // Extract relative path from agent folder for nested agents - let agentName = path.basename(item, ".md") - const agentFolderPath = item.includes("/.opencode/agent/") - ? item.split("/.opencode/agent/")[1] - : item.includes("/agent/") - ? item.split("/agent/")[1] - : agentName + ".md" - - // If agent is in a subfolder, include folder path in name - if (agentFolderPath.includes("/")) { - const relativePath = agentFolderPath.replace(".md", "") - const pathParts = relativePath.split("/") - agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1] - } - - const config = { - name: agentName, - ...md.data, - prompt: md.content.trim(), - } - const parsed = Agent.safeParse(config) - if (parsed.success) { - result.agent = mergeDeep(result.agent, { - [config.name]: parsed.data, - }) - continue - } - throw new InvalidError({ path: item }, { cause: parsed.error }) + for (const dir of directories) { + await assertValid(dir) + result.command = mergeDeep(result.command ?? {}, await loadCommand(dir)) + result.agent = mergeDeep(result.agent, await loadAgent(dir)) + result.agent = mergeDeep(result.agent, await loadMode(dir)) + result.plugin.push(...(await loadPlugin(dir))) } - // Load mode markdown files - result.mode = result.mode || {} - const markdownModes = [ - ...(await Filesystem.globUp("mode/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/mode/*.md", Instance.directory, Instance.worktree)), - ] - for (const item of markdownModes) { - const content = await Bun.file(item).text() - const md = matter(content) - if (!md.data) continue - - const config = { - name: path.basename(item, ".md"), - ...md.data, - prompt: md.content.trim(), - } - const parsed = Agent.safeParse(config) - if (parsed.success) { - result.agent = mergeDeep(result.mode, { - [config.name]: { - ...parsed.data, - mode: "primary" as const, - }, - }) - continue - } - } - - // Load command markdown files - result.command = result.command || {} - const markdownCommands = [ - ...(await Filesystem.globUp("command/**/*.md", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/command/**/*.md", Instance.directory, Instance.worktree)), - ] - for (const item of markdownCommands) { - const content = await Bun.file(item).text() - const md = matter(content) - if (!md.data) continue - - const name = (() => { - const patterns = ["/.opencode/command/", "/command/"] - const pattern = patterns.find((p) => item.includes(p)) - - if (pattern) { - const index = item.indexOf(pattern) - return item.slice(index + pattern.length, -3) - } - return path.basename(item, ".md") - })() - - const config = { - name, - ...md.data, - template: md.content.trim(), - } - const parsed = Command.safeParse(config) - if (parsed.success) { - result.command = mergeDeep(result.command, { - [config.name]: parsed.data, - }) - continue - } - throw new InvalidError({ path: item }, { cause: parsed.error }) - } // Migrate deprecated mode field to agent field for (const [name, mode] of Object.entries(result.mode)) { result.agent = mergeDeep(result.agent ?? {}, { @@ -170,14 +77,6 @@ export namespace Config { }) } - result.plugin = result.plugin || [] - result.plugin.push( - ...[ - ...(await Filesystem.globUp("plugin/*.{ts,js}", Global.Path.config, Global.Path.config)), - ...(await Filesystem.globUp(".opencode/plugin/*.{ts,js}", Instance.directory, Instance.worktree)), - ].map((x) => "file://" + x), - ) - if (Flag.OPENCODE_PERMISSION) { result.permission = mergeDeep(result.permission ?? {}, JSON.parse(Flag.OPENCODE_PERMISSION)) } @@ -218,6 +117,128 @@ export namespace Config { } }) + async function assertValid(dir: string) { + const ALLOWED_DIRS = new Set(["agent", "command", "mode", "plugin"]) + const UNEXPECTED_DIR_GLOB = new Bun.Glob("*/") + for await (const item of UNEXPECTED_DIR_GLOB.scan({ absolute: true, cwd: dir, onlyFiles: false })) { + const dirname = path.basename(item) + if (!ALLOWED_DIRS.has(dirname)) { + throw new InvalidError({ + path: dir, + message: `Unexpected directory "${dirname}" found in "${dir}". Only ${ALLOWED_DIRS.values().toArray().join(", ")} directories are allowed.`, + }) + } + } + } + + const COMMAND_GLOB = new Bun.Glob("command/**/*.md") + async function loadCommand(dir: string) { + const result: Record = {} + for await (const item of COMMAND_GLOB.scan({ absolute: true, cwd: dir })) { + const content = await Bun.file(item).text() + const md = matter(content) + if (!md.data) continue + + const name = (() => { + const patterns = ["/.opencode/command/", "/command/"] + const pattern = patterns.find((p) => item.includes(p)) + + if (pattern) { + const index = item.indexOf(pattern) + return item.slice(index + pattern.length, -3) + } + return path.basename(item, ".md") + })() + + const config = { + name, + ...md.data, + template: md.content.trim(), + } + const parsed = Command.safeParse(config) + if (parsed.success) { + result[config.name] = parsed.data + continue + } + throw new InvalidError({ path: item }, { cause: parsed.error }) + } + return result + } + + const AGENT_GLOB = new Bun.Glob("agent/**/*.md") + async function loadAgent(dir: string) { + const result: Record = {} + + for await (const item of AGENT_GLOB.scan({ absolute: true, cwd: dir })) { + const content = await Bun.file(item).text() + const md = matter(content) + if (!md.data) continue + + // Extract relative path from agent folder for nested agents + let agentName = path.basename(item, ".md") + const agentFolderPath = item.includes("/.opencode/agent/") + ? item.split("/.opencode/agent/")[1] + : item.includes("/agent/") + ? item.split("/agent/")[1] + : agentName + ".md" + + // If agent is in a subfolder, include folder path in name + if (agentFolderPath.includes("/")) { + const relativePath = agentFolderPath.replace(".md", "") + const pathParts = relativePath.split("/") + agentName = pathParts.slice(0, -1).join("/") + "/" + pathParts[pathParts.length - 1] + } + + const config = { + name: agentName, + ...md.data, + prompt: md.content.trim(), + } + const parsed = Agent.safeParse(config) + if (parsed.success) { + result[config.name] = parsed.data + continue + } + throw new InvalidError({ path: item }, { cause: parsed.error }) + } + return result + } + + const MODE_GLOB = new Bun.Glob("mode/*.md") + async function loadMode(dir: string) { + const result: Record = {} + for await (const item of MODE_GLOB.scan({ absolute: true, cwd: dir })) { + const content = await Bun.file(item).text() + const md = matter(content) + if (!md.data) continue + + const config = { + name: path.basename(item, ".md"), + ...md.data, + prompt: md.content.trim(), + } + const parsed = Agent.safeParse(config) + if (parsed.success) { + result[config.name] = { + ...parsed.data, + mode: "primary" as const, + } + continue + } + } + return result + } + + const PLUGIN_GLOB = new Bun.Glob("plugin/*.{ts,js}") + async function loadPlugin(dir: string) { + const plugins: string[] = [] + + for await (const item of PLUGIN_GLOB.scan({ absolute: true, cwd: dir })) { + plugins.push("file://" + item) + } + return plugins + } + export const McpLocal = z .object({ type: z.literal("local").describe("Type of MCP server connection"),