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");
+    }
 }

Reply via email to