This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/camel-kit.git
commit f2b151be86d4cb1742c4f6d8360a8a73ec5bb78c Author: Jinal Sarvaiya <[email protected]> AuthorDate: Fri Nov 6 14:41:22 2026 +0000 fix #99: custom-dir/managed config, fail-closed on substitutions --- .../camel/kit/generator/OpenCodeGenerator.java | 36 +++++- .../camel/kit/generator/OpenCodeGeneratorTest.java | 121 +++++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) diff --git a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/generator/OpenCodeGenerator.java b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/generator/OpenCodeGenerator.java index ab1abcf0..6d7201b6 100644 --- a/kit/camel-kit-core/src/main/java/org/apache/camel/kit/generator/OpenCodeGenerator.java +++ b/kit/camel-kit-core/src/main/java/org/apache/camel/kit/generator/OpenCodeGenerator.java @@ -8,6 +8,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; import org.apache.camel.kit.config.OpenCodeProjectConfig; import org.apache.camel.kit.util.AnsiColors; @@ -24,6 +25,8 @@ public class OpenCodeGenerator extends DefaultGenerator { private static final Set<String> BUILTIN_PERMISSION_PREFIXES = Set.of("external"); + private static final Pattern SUBSTITUTION_PATTERN = Pattern.compile("\\{(env|file):[^}]+}"); + private static final List<String> GLOBAL_CONFIG_FILES = List.of("opencode.json", "opencode.jsonc"); private static final List<String> ANCESTOR_CONFIG_FILES = List.of( @@ -49,8 +52,13 @@ public class OpenCodeGenerator extends DefaultGenerator { List<String> mcpServers = readMcpServerNames(ctx); Set<String> seen = new LinkedHashSet<>(); boolean collisionDetected = false; + boolean unresolvedSubstitution = false; StringBuilder denials = new StringBuilder(); for (String server : mcpServers) { + if (SUBSTITUTION_PATTERN.matcher(server).find()) { + unresolvedSubstitution = true; + continue; + } String normalized = normalizeServerName(server); if (BUILTIN_PERMISSION_PREFIXES.contains(normalized)) { collisionDetected = true; @@ -61,7 +69,7 @@ public class OpenCodeGenerator extends DefaultGenerator { data.put("MCP_TOOL_DENIALS", denials.toString()); // OpenCode-specific: generate agent definitions - generateAgents(ctx, data, !collisionDetected); + generateAgents(ctx, data, !collisionDetected && !unresolvedSubstitution); } static String normalizeServerName(String name) { @@ -88,6 +96,8 @@ public class OpenCodeGenerator extends DefaultGenerator { for (Path globalFile : globalConfigFiles()) { collectMcpServersFromFile(globalFile, names); } + collectMcpServersFromDir(customConfigDir(), names); + collectMcpServersFromDir(managedConfigDir(), names); Path explicitConfig = explicitConfigFile(); if (explicitConfig != null) { collectMcpServersFromFile(explicitConfig, names); @@ -99,6 +109,18 @@ public class OpenCodeGenerator extends DefaultGenerator { return List.copyOf(names); } + private static void collectMcpServersFromDir(Path dir, Set<String> names) throws IOException { + if (dir == null) { + return; + } + for (String fileName : GLOBAL_CONFIG_FILES) { + Path candidate = dir.resolve(fileName); + if (Files.exists(candidate)) { + collectMcpServersFromFile(candidate, names); + } + } + } + static void collectMcpServersFromFile(Path configFile, Set<String> names) throws IOException { collectMcpServersFromNode( OpenCodeProjectConfig.newJsonMapper().readTree(configFile.toFile()), names); @@ -182,6 +204,18 @@ public class OpenCodeGenerator extends DefaultGenerator { return null; } + Path customConfigDir() { + String dir = System.getenv("OPENCODE_CONFIG_DIR"); + if (dir != null && !dir.isBlank()) { + return Path.of(dir); + } + return null; + } + + Path managedConfigDir() { + return Path.of("/etc/opencode"); + } + String inlineConfigContent() { return System.getenv("OPENCODE_CONFIG_CONTENT"); } diff --git a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/OpenCodeGeneratorTest.java b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/OpenCodeGeneratorTest.java index 82f130f8..353b99ed 100644 --- a/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/OpenCodeGeneratorTest.java +++ b/kit/camel-kit-core/src/test/java/org/apache/camel/kit/generator/OpenCodeGeneratorTest.java @@ -1289,4 +1289,125 @@ class OpenCodeGeneratorTest { assertTrue(worker.contains("probe_*: deny"), "Ship worker must deny other servers in the same native container"); } + + @Test + void shipWorkerDeniesServersFromCustomConfigDir() throws Exception { + Path customDir = tempDir.resolve("fake-custom-dir"); + Files.createDirectories(customDir); + Files.writeString(customDir.resolve("opencode.json"), """ + { + "mcp": { + "custom-dir-probe": {"type": "local", "command": ["echo"], "enabled": true} + } + } + """); + + OpenCodeGenerator gen = new OpenCodeGenerator() { + @Override + Path customConfigDir() { + return customDir; + } + }; + InitContext ctx = createContext(); + gen.generate(ctx); + + String worker = Files.readString( + tempDir.resolve(".opencode/agents/camel-ship-worker.md")); + assertTrue(worker.contains("custom-dir-probe_*: deny"), + "Ship worker must deny MCP servers from OPENCODE_CONFIG_DIR"); + } + + @Test + void shipWorkerDeniesServersFromManagedConfig() throws Exception { + Path managedDir = tempDir.resolve("fake-managed"); + Files.createDirectories(managedDir); + Files.writeString(managedDir.resolve("opencode.json"), """ + { + "mcp": { + "managed-probe": {"type": "local", "command": ["echo"], "enabled": true} + } + } + """); + + OpenCodeGenerator gen = new OpenCodeGenerator() { + @Override + Path managedConfigDir() { + return managedDir; + } + }; + InitContext ctx = createContext(); + gen.generate(ctx); + + String worker = Files.readString( + tempDir.resolve(".opencode/agents/camel-ship-worker.md")); + assertTrue(worker.contains("managed-probe_*: deny"), + "Ship worker must deny MCP servers from platform managed config"); + } + + @Test + void shipWorkerNotGeneratedWhenServerNameHasEnvSubstitution() throws Exception { + OpenCodeGenerator gen = new OpenCodeGenerator() { + @Override + String inlineConfigContent() { + return """ + { + "mcp": { + "{env:MY_MCP_SERVER}": {"type": "local", "command": ["echo"]} + } + } + """; + } + }; + InitContext ctx = createContext(); + gen.generate(ctx); + + Path workerFile = tempDir.resolve(".opencode/agents/camel-ship-worker.md"); + assertFalse(Files.exists(workerFile), + "Ship worker must not be generated when server names contain unresolved env substitutions"); + } + + @Test + void shipWorkerNotGeneratedWhenServerNameHasFileSubstitution() throws Exception { + OpenCodeGenerator gen = new OpenCodeGenerator() { + @Override + String inlineConfigContent() { + return """ + { + "mcp": { + "{file:server-name.txt}": {"type": "local", "command": ["echo"]} + } + } + """; + } + }; + InitContext ctx = createContext(); + gen.generate(ctx); + + Path workerFile = tempDir.resolve(".opencode/agents/camel-ship-worker.md"); + assertFalse(Files.exists(workerFile), + "Ship worker must not be generated when server names contain unresolved file substitutions"); + } + + @Test + void shipWorkerGeneratedWhenSubstitutionInProjectConfig() throws Exception { + InitContext ctx = createContext(); + new OpenCodeGenerator().generate(ctx); + + Path configFile = tempDir.resolve("opencode.json"); + com.fasterxml.jackson.databind.node.ObjectNode config + = (com.fasterxml.jackson.databind.node.ObjectNode) new ObjectMapper().readTree(configFile.toFile()); + com.fasterxml.jackson.databind.node.ObjectNode probe = new ObjectMapper().createObjectNode(); + probe.put("type", "local"); + probe.set("command", new ObjectMapper().createArrayNode().add("echo")); + ((com.fasterxml.jackson.databind.node.ObjectNode) config.path("mcp")) + .set("{env:DYNAMIC_SERVER}", probe); + Files.writeString(configFile, new ObjectMapper().writerWithDefaultPrettyPrinter() + .writeValueAsString(config)); + + new OpenCodeGenerator().generate(ctx); + + Path workerFile = tempDir.resolve(".opencode/agents/camel-ship-worker.md"); + assertFalse(Files.exists(workerFile), + "Ship worker must not be generated when project config has unresolved substitution"); + } }
