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 ad749a3924bdf50101c34ab8e3049cf2f1a3ed54
Author: Jinal Sarvaiya <[email protected]>
AuthorDate: Thu Nov 5 14:21:15 2026 +0000

    fix: fail-closed collision, global config, ASCII normalization
---
 .../camel/kit/generator/OpenCodeGenerator.java     | 82 +++++++++++++++++-----
 .../camel/kit/generator/OpenCodeGeneratorTest.java | 72 +++++++++++++++++--
 2 files changed, 131 insertions(+), 23 deletions(-)

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 bdc3a948..3f3f2743 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
@@ -24,6 +24,8 @@ public class OpenCodeGenerator extends DefaultGenerator {
 
     private static final Set<String> BUILTIN_PERMISSION_PREFIXES = 
Set.of("external");
 
+    private static final List<String> GLOBAL_CONFIG_FILES = 
List.of("opencode.json", "opencode.jsonc");
+
     private final QuteTemplateEngine templateEngine = new QuteTemplateEngine();
 
     @Override
@@ -42,57 +44,105 @@ public class OpenCodeGenerator extends DefaultGenerator {
 
         List<String> mcpServers = readMcpServerNames(ctx);
         Set<String> seen = new LinkedHashSet<>();
+        boolean collisionDetected = false;
         StringBuilder denials = new StringBuilder();
         for (String server : mcpServers) {
             String normalized = normalizeServerName(server);
             if (BUILTIN_PERMISSION_PREFIXES.contains(normalized)) {
-                continue;
-            }
-            if (seen.add(normalized)) {
+                collisionDetected = true;
+            } else if (seen.add(normalized)) {
                 denials.append("  ").append(normalized).append("_*: deny\n");
             }
         }
         data.put("MCP_TOOL_DENIALS", denials.toString());
 
         // OpenCode-specific: generate agent definitions
-        generateAgents(ctx, data);
+        generateAgents(ctx, data, !collisionDetected);
     }
 
     static String normalizeServerName(String name) {
         StringBuilder sb = new StringBuilder(name.length());
         for (int i = 0; i < name.length(); i++) {
             char c = name.charAt(i);
-            sb.append(Character.isLetterOrDigit(c) || c == '-' ? c : '_');
+            sb.append(isAsciiAlphanumericOrHyphen(c) ? c : '_');
         }
         return sb.toString();
     }
 
+    private static boolean isAsciiAlphanumericOrHyphen(char c) {
+        return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' 
&& c <= '9') || c == '-';
+    }
+
     private List<String> readMcpServerNames(InitContext ctx) throws 
IOException {
         Set<String> names = new LinkedHashSet<>();
         for (Path configFile : 
OpenCodeProjectConfig.existingFiles(ctx.projectDir())) {
-            JsonNode config = 
OpenCodeProjectConfig.newJsonMapper().readTree(configFile.toFile());
-            if (config == null) {
-                continue;
-            }
-            JsonNode mcp = config.path("mcp");
-            if (!mcp.isObject()) {
-                continue;
-            }
-            mcp.fieldNames().forEachRemaining(names::add);
+            collectMcpServersFromFile(configFile, names);
+        }
+        for (Path globalFile : globalConfigFiles()) {
+            collectMcpServersFromFile(globalFile, names);
         }
         return List.copyOf(names);
     }
 
-    private void generateAgents(InitContext ctx, Map<String, Object> data) 
throws Exception {
+    private static void collectMcpServersFromFile(Path configFile, Set<String> 
names) throws IOException {
+        JsonNode config = 
OpenCodeProjectConfig.newJsonMapper().readTree(configFile.toFile());
+        if (config == null) {
+            return;
+        }
+        JsonNode mcp = config.path("mcp");
+        if (!mcp.isObject()) {
+            return;
+        }
+        JsonNode servers = mcp.path("servers");
+        boolean hasNativeContainer = servers.isObject() && 
!servers.has("type");
+        if (hasNativeContainer) {
+            servers.fields().forEachRemaining(entry -> {
+                if (entry.getValue().isObject()) {
+                    names.add(entry.getKey());
+                }
+            });
+        }
+        mcp.fields().forEachRemaining(entry -> {
+            if (entry.getValue().isObject()
+                    && !(hasNativeContainer && 
"servers".equals(entry.getKey()))) {
+                names.add(entry.getKey());
+            }
+        });
+    }
+
+    static List<Path> globalConfigFiles() {
+        Path globalDir = globalConfigDir();
+        return GLOBAL_CONFIG_FILES.stream()
+                .map(globalDir::resolve)
+                .filter(Files::exists)
+                .toList();
+    }
+
+    private static Path globalConfigDir() {
+        String xdgConfigHome = System.getenv("XDG_CONFIG_HOME");
+        if (xdgConfigHome != null && !xdgConfigHome.isBlank()) {
+            return Path.of(xdgConfigHome, "opencode");
+        }
+        return Path.of(System.getProperty("user.home"), ".config", "opencode");
+    }
+
+    private void generateAgents(InitContext ctx, Map<String, Object> data, 
boolean includeShipWorker)
+            throws Exception {
         Path agentsDir = ctx.projectDir().resolve(".opencode/agents");
         Files.createDirectories(agentsDir);
 
+        int count = 0;
         for (String agentName : AGENTS) {
+            if ("camel-ship-worker".equals(agentName) && !includeShipWorker) {
+                Files.deleteIfExists(agentsDir.resolve(agentName + ".md"));
+                continue;
+            }
             String content = 
templateEngine.render("templates/opencode/agents/" + agentName + ".md", data);
             Files.writeString(agentsDir.resolve(agentName + ".md"), content);
+            count++;
         }
 
-        ctx.printer().println(AnsiColors.green("✓") + " Generated " + 
AGENTS.length + " OpenCode agent definitions");
+        ctx.printer().println(AnsiColors.green("✓") + " Generated " + count + 
" OpenCode agent definitions");
     }
 
 }
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 7894503f..f5514d1c 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
@@ -1073,7 +1073,7 @@ class OpenCodeGeneratorTest {
     }
 
     @Test
-    void shipWorkerSkipsExternalServerToAvoidBuiltinCollision() throws 
Exception {
+    void shipWorkerNotGeneratedWhenExternalServerCollidesWithBuiltin() throws 
Exception {
         InitContext ctx = createContext();
         new OpenCodeGenerator().generate(ctx);
 
@@ -1089,13 +1089,71 @@ class OpenCodeGeneratorTest {
 
         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 an MCP server collides 
with a built-in permission prefix");
+        
assertTrue(Files.exists(tempDir.resolve(".opencode/agents/brainstormer.md")),
+                "Other agent definitions must still be generated");
+    }
+
+    @Test
+    void shipWorkerDeletedOnReInitWhenCollisionAppears() throws Exception {
+        InitContext ctx = createContext();
+        new OpenCodeGenerator().generate(ctx);
+
+        Path workerFile = 
tempDir.resolve(".opencode/agents/camel-ship-worker.md");
+        assertTrue(Files.exists(workerFile), "Worker must exist after initial 
generation");
+
+        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 external = new 
ObjectMapper().createObjectNode();
+        external.put("type", "remote");
+        external.put("url", "https://example.test/external";);
+        ((com.fasterxml.jackson.databind.node.ObjectNode) 
config.path("mcp")).set("external", external);
+        Files.writeString(configFile, new 
ObjectMapper().writerWithDefaultPrettyPrinter()
+                .writeValueAsString(config));
+
+        new OpenCodeGenerator().generate(ctx);
+
+        assertFalse(Files.exists(workerFile),
+                "Ship worker must be deleted on re-init when collision is 
detected");
+    }
+
+    @Test
+    void shipWorkerDeniesNativeFormatServers() throws Exception {
+        InitContext ctx = createContext();
+
+        Files.writeString(tempDir.resolve("opencode.json"), """
+                {
+                  "mcp": {
+                    "servers": {
+                      "probe": {"type": "local", "command": ["echo"], 
"enabled": true},
+                      "scanner": {"type": "local", "command": ["echo"], 
"enabled": true}
+                    }
+                  }
+                }
+                """);
+
+        new OpenCodeGenerator().generate(ctx);
+
         String worker = Files.readString(
                 tempDir.resolve(".opencode/agents/camel-ship-worker.md"));
-        assertFalse(worker.contains("external_*: deny"),
-                "Ship worker must not emit external_* denial to avoid 
colliding with built-in external_directory");
-        assertTrue(worker.contains("mcp: deny"),
-                "Ship worker must still deny all MCP tools via the mcp 
permission");
-        assertTrue(worker.contains("camel_*: deny"),
-                "Ship worker must still deny other MCP server tools");
+        assertTrue(worker.contains("probe_*: deny"),
+                "Ship worker must deny servers declared in native mcp.servers 
format");
+        assertTrue(worker.contains("scanner_*: deny"),
+                "Ship worker must deny all servers from native mcp.servers 
container");
+    }
+
+    @Test
+    void shipWorkerUsesAsciiOnlyNormalization() throws Exception {
+        assertEquals("caf_", OpenCodeGenerator.normalizeServerName("café"),
+                "Non-ASCII letters must be replaced with underscore");
+        assertEquals("probe_local", 
OpenCodeGenerator.normalizeServerName("probe.local"),
+                "Dots must be replaced with underscore");
+        assertEquals("my-server", 
OpenCodeGenerator.normalizeServerName("my-server"),
+                "Hyphens must be preserved");
+        assertEquals("__", OpenCodeGenerator.normalizeServerName("üñ"),
+                "All non-ASCII characters must be replaced");
     }
 }

Reply via email to