From 6ac4a1f17062d0fbb7e92f5a52353d1b94a0a93b Mon Sep 17 00:00:00 2001 From: Gunju Kim Date: Tue, 9 Jun 2026 07:16:00 +0000 Subject: [PATCH] Integrate Kanon agent config rendering --- agent-images/kanon_env.sh | 29 ++++++ claude-code/Dockerfile | 6 +- claude-code/kelos_entrypoint.sh | 40 +++++---- codex/Dockerfile | 6 +- codex/kelos_entrypoint.sh | 68 ++++++++------ cursor/Dockerfile | 6 +- docs/agent-image-interface.md | 12 ++- gemini/Dockerfile | 6 +- internal/controller/job_builder.go | 72 +++++++++++++-- internal/controller/job_builder_test.go | 114 +++++++++++++++++++++++- opencode/Dockerfile | 6 +- 11 files changed, 308 insertions(+), 57 deletions(-) create mode 100644 agent-images/kanon_env.sh diff --git a/agent-images/kanon_env.sh b/agent-images/kanon_env.sh new file mode 100644 index 000000000..a7dd09f1b --- /dev/null +++ b/agent-images/kanon_env.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +apply_kanon_env_config() { + agent="${1:?Agent argument is required}" + + if [ -z "${KELOS_KANON_CONFIG:-}" ]; then + return 1 + fi + + if ! command -v kanon >/dev/null 2>&1; then + echo "Warning: KELOS_KANON_CONFIG is set but kanon is unavailable; using legacy agent config setup" >&2 + return 1 + fi + + kanon_home="$(mktemp -d "${TMPDIR:-/tmp}/kelos-kanon.XXXXXX")" + mkdir -p "$kanon_home/instructions" + printf '%s' "$KELOS_KANON_CONFIG" >"$kanon_home/kanon.yaml" + + if [ -n "${KELOS_AGENTS_MD:-}" ]; then + printf '%s' "$KELOS_AGENTS_MD" >"$kanon_home/instructions/kelos.md" + fi + + if ! kanon apply --home "$kanon_home" --agent "$agent" --yes >/dev/null; then + echo "Failed to apply Kanon agent config" >&2 + exit 1 + fi + + return 0 +} diff --git a/claude-code/Dockerfile b/claude-code/Dockerfile index 8c8884ea0..5ef791f5c 100644 --- a/claude-code/Dockerfile +++ b/claude-code/Dockerfile @@ -27,11 +27,15 @@ RUN ARCH=$(dpkg --print-architecture) \ ENV PATH="/usr/local/go/bin:${PATH}" +ARG KANON_VERSION=3edde4571b0aad2530627d94c8d2624ab44a3952 +RUN GOBIN=/usr/local/bin go install github.com/kelos-dev/kanon@${KANON_VERSION} + ARG CLAUDE_CODE_VERSION=2.1.163 RUN npm install -g @anthropic-ai/claude-code@${CLAUDE_CODE_VERSION} COPY claude-code/kelos_entrypoint.sh /kelos_entrypoint.sh -RUN chmod +x /kelos_entrypoint.sh +COPY agent-images/kanon_env.sh /kelos/kanon_env.sh +RUN chmod +x /kelos_entrypoint.sh /kelos/kanon_env.sh ARG TARGETARCH COPY bin/kelos-capture-linux-${TARGETARCH} /kelos/kelos-capture diff --git a/claude-code/kelos_entrypoint.sh b/claude-code/kelos_entrypoint.sh index e5e813a53..730ded06f 100755 --- a/claude-code/kelos_entrypoint.sh +++ b/claude-code/kelos_entrypoint.sh @@ -27,24 +27,26 @@ if [ -n "${KELOS_EFFORT:-}" ]; then ARGS+=("--effort" "$KELOS_EFFORT") fi -# Write user-level instructions (additive, no conflict with repo) -if [ -n "${KELOS_AGENTS_MD:-}" ]; then - mkdir -p ~/.claude - printf '%s' "$KELOS_AGENTS_MD" >~/.claude/CLAUDE.md +KANON_CONFIG_APPLIED="" +if [ -f /kelos/kanon_env.sh ]; then + . /kelos/kanon_env.sh + if apply_kanon_env_config claude; then + KANON_CONFIG_APPLIED="true" + fi fi -# Pass each plugin directory via --plugin-dir -if [ -n "${KELOS_PLUGIN_DIR:-}" ] && [ -d "${KELOS_PLUGIN_DIR}" ]; then - for dir in "${KELOS_PLUGIN_DIR}"/*/; do - [ -d "$dir" ] && ARGS+=("--plugin-dir" "$dir") - done -fi +if [ "$KANON_CONFIG_APPLIED" != "true" ]; then + # Write user-level instructions (additive, no conflict with repo) + if [ -n "${KELOS_AGENTS_MD:-}" ]; then + mkdir -p ~/.claude + printf '%s' "$KELOS_AGENTS_MD" >~/.claude/CLAUDE.md + fi -# Write MCP server configuration to user-scoped ~/.claude.json. -# This avoids overwriting the repository's own .mcp.json while -# still making the servers available to Claude Code. -if [ -n "${KELOS_MCP_SERVERS:-}" ]; then - node -e ' + # Write MCP server configuration to user-scoped ~/.claude.json. + # This avoids overwriting the repository's own .mcp.json while + # still making the servers available to Claude Code. + if [ -n "${KELOS_MCP_SERVERS:-}" ]; then + node -e ' const fs = require("fs"); const cfgPath = require("os").homedir() + "/.claude.json"; let existing = {}; @@ -53,6 +55,14 @@ const mcp = JSON.parse(process.env.KELOS_MCP_SERVERS); existing.mcpServers = Object.assign(existing.mcpServers || {}, mcp.mcpServers || {}); fs.writeFileSync(cfgPath, JSON.stringify(existing, null, 2)); ' + fi +fi + +# Pass each plugin directory via --plugin-dir +if [ -n "${KELOS_PLUGIN_DIR:-}" ] && [ -d "${KELOS_PLUGIN_DIR}" ]; then + for dir in "${KELOS_PLUGIN_DIR}"/*/; do + [ -d "$dir" ] && ARGS+=("--plugin-dir" "$dir") + done fi # Run pre-agent setup command if configured. KELOS_SETUP_COMMAND is the diff --git a/codex/Dockerfile b/codex/Dockerfile index 1bf2b917b..acdb96299 100644 --- a/codex/Dockerfile +++ b/codex/Dockerfile @@ -27,15 +27,19 @@ RUN ARCH=$(dpkg --print-architecture) \ ENV PATH="/usr/local/go/bin:${PATH}" +ARG KANON_VERSION=3edde4571b0aad2530627d94c8d2624ab44a3952 +RUN GOBIN=/usr/local/bin go install github.com/kelos-dev/kanon@${KANON_VERSION} + ARG CODEX_VERSION=0.137.0 RUN npm install -g @openai/codex@${CODEX_VERSION} COPY codex/kelos_entrypoint.sh /kelos_entrypoint.sh +COPY agent-images/kanon_env.sh /kelos/kanon_env.sh ARG TARGETARCH COPY bin/kelos-capture-linux-${TARGETARCH} /kelos/kelos-capture COPY bin/kelos-codex-auth-refresh-linux-${TARGETARCH} /kelos/kelos-codex-auth-refresh -RUN chmod +x /kelos_entrypoint.sh /kelos/kelos-codex-auth-refresh +RUN chmod +x /kelos_entrypoint.sh /kelos/kanon_env.sh /kelos/kelos-codex-auth-refresh RUN useradd -u 61100 -m -s /bin/bash agent RUN mkdir -p /home/agent/.codex /home/agent/.local/bin && chown -R agent:agent /home/agent diff --git a/codex/kelos_entrypoint.sh b/codex/kelos_entrypoint.sh index 7ca9c2488..3123febea 100755 --- a/codex/kelos_entrypoint.sh +++ b/codex/kelos_entrypoint.sh @@ -37,38 +37,27 @@ if [ -n "${KELOS_EFFORT:-}" ]; then ARGS+=("--config" "model_reasoning_effort=\"$SAFE_EFFORT\"") fi -# Write user-level instructions (global scope read by Codex CLI) -if [ -n "${KELOS_AGENTS_MD:-}" ]; then - mkdir -p ~/.codex - printf '%s' "$KELOS_AGENTS_MD" >~/.codex/AGENTS.md +KANON_CONFIG_APPLIED="" +if [ -f /kelos/kanon_env.sh ]; then + . /kelos/kanon_env.sh + if apply_kanon_env_config codex; then + KANON_CONFIG_APPLIED="true" + fi fi -# Install each plugin as a Codex skill directory under ~/.codex/skills -if [ -n "${KELOS_PLUGIN_DIR:-}" ] && [ -d "${KELOS_PLUGIN_DIR}" ]; then - for plugindir in "${KELOS_PLUGIN_DIR}"/*/; do - [ -d "$plugindir" ] || continue - # Copy skills into ~/.codex/skills///SKILL.md - if [ -d "${plugindir}skills" ]; then - for skilldir in "${plugindir}skills"/*/; do - [ -d "$skilldir" ] || continue - skillname=$(basename "$skilldir") - pluginname=$(basename "$plugindir") - targetdir="$HOME/.codex/skills/${pluginname}-${skillname}" - mkdir -p "$targetdir" - if [ -f "${skilldir}SKILL.md" ]; then - cp "${skilldir}SKILL.md" "$targetdir/SKILL.md" - fi - done - fi - done -fi +if [ "$KANON_CONFIG_APPLIED" != "true" ]; then + # Write user-level instructions (global scope read by Codex CLI) + if [ -n "${KELOS_AGENTS_MD:-}" ]; then + mkdir -p ~/.codex + printf '%s' "$KELOS_AGENTS_MD" >~/.codex/AGENTS.md + fi -# Write MCP server configuration to project-scoped config. -# KELOS_MCP_SERVERS contains JSON in .mcp.json format; convert to -# Codex TOML via a small Node.js helper that is available in the image. -if [ -n "${KELOS_MCP_SERVERS:-}" ]; then - mkdir -p ~/.codex - node -e ' + # Write MCP server configuration to project-scoped config. + # KELOS_MCP_SERVERS contains JSON in .mcp.json format; convert to + # Codex TOML via a small Node.js helper that is available in the image. + if [ -n "${KELOS_MCP_SERVERS:-}" ]; then + mkdir -p ~/.codex + node -e ' const cfg = JSON.parse(process.env.KELOS_MCP_SERVERS); const servers = cfg.mcpServers || {}; let toml = ""; @@ -89,6 +78,27 @@ for (const [name, s] of Object.entries(servers)) { } process.stdout.write(toml); ' >>~/.codex/config.toml + fi +fi + +# Install each plugin as a Codex skill directory under ~/.codex/skills +if [ -n "${KELOS_PLUGIN_DIR:-}" ] && [ -d "${KELOS_PLUGIN_DIR}" ]; then + for plugindir in "${KELOS_PLUGIN_DIR}"/*/; do + [ -d "$plugindir" ] || continue + # Copy skills into ~/.codex/skills///SKILL.md + if [ -d "${plugindir}skills" ]; then + for skilldir in "${plugindir}skills"/*/; do + [ -d "$skilldir" ] || continue + skillname=$(basename "$skilldir") + pluginname=$(basename "$plugindir") + targetdir="$HOME/.codex/skills/${pluginname}-${skillname}" + mkdir -p "$targetdir" + if [ -f "${skilldir}SKILL.md" ]; then + cp "${skilldir}SKILL.md" "$targetdir/SKILL.md" + fi + done + fi + done fi # Run pre-agent setup command if configured. KELOS_SETUP_COMMAND is the diff --git a/cursor/Dockerfile b/cursor/Dockerfile index 20d2c88fc..34f056dfe 100644 --- a/cursor/Dockerfile +++ b/cursor/Dockerfile @@ -27,8 +27,12 @@ RUN ARCH=$(dpkg --print-architecture) \ ENV PATH="/usr/local/go/bin:${PATH}" +ARG KANON_VERSION=3edde4571b0aad2530627d94c8d2624ab44a3952 +RUN GOBIN=/usr/local/bin go install github.com/kelos-dev/kanon@${KANON_VERSION} + COPY cursor/kelos_entrypoint.sh /kelos_entrypoint.sh -RUN chmod +x /kelos_entrypoint.sh +COPY agent-images/kanon_env.sh /kelos/kanon_env.sh +RUN chmod +x /kelos_entrypoint.sh /kelos/kanon_env.sh ARG TARGETARCH COPY bin/kelos-capture-linux-${TARGETARCH} /kelos/kelos-capture diff --git a/docs/agent-image-interface.md b/docs/agent-image-interface.md index 6d5f09e14..adf254b86 100644 --- a/docs/agent-image-interface.md +++ b/docs/agent-image-interface.md @@ -44,7 +44,9 @@ Kelos sets the following reserved environment variables on agent containers: | `KELOS_AGENT_TYPE` | The agent type (`claude-code`, `codex`, `gemini`, `opencode`, `cursor`) | Always | | `KELOS_BASE_BRANCH` | The base branch (workspace `ref`) for the task | When workspace has a non-empty `ref` | | `KELOS_AGENTS_MD` | User-level instructions from AgentConfig | When `agentConfigRef` is set and `agentsMD` is non-empty | -| `KELOS_PLUGIN_DIR` | Path to plugin directory containing skills and agents | When `agentConfigRef` is set and `plugins` is non-empty | +| `KELOS_PLUGIN_DIR` | Path to plugin directory containing skills and agents | When `agentConfigRef` is set and `plugins` or `skills` is non-empty | +| `KELOS_MCP_SERVERS` | JSON MCP server config from AgentConfig | When `agentConfigRef` is set and `mcpServers` is non-empty | +| `KELOS_KANON_CONFIG` | Kanon source config generated from AgentConfig instructions and MCP servers | When `agentConfigRef` is set and `agentsMD` or `mcpServers` is non-empty | | `KELOS_SETUP_COMMAND` | JSON-encoded exec-form array from `Workspace.spec.setupCommand`, executed by the entrypoint before the agent starts | When the workspace defines `setupCommand` | > The names listed in this table are reserved for Kelos behavior. When Kelos @@ -62,6 +64,14 @@ The bundled agent images handle `KELOS_EFFORT` as follows: - `opencode`: writes agent model options including `reasoningEffort` and provider variants where available. - `cursor`: adds effort guidance to user-level instructions because Cursor CLI does not expose a documented effort flag. +The bundled agent images include the `kanon` binary. The `codex` and +`claude-code` entrypoints use `KELOS_KANON_CONFIG` to apply AgentConfig +instructions and MCP servers through Kanon, then keep the existing +`KELOS_PLUGIN_DIR` handling for plugins. The `gemini`, `opencode`, and +`cursor` entrypoints continue to use the legacy `KELOS_AGENTS_MD`, +`KELOS_MCP_SERVERS`, and `KELOS_PLUGIN_DIR` paths until Kanon provides matching +adapters for those agents. + ### 4. User ID The agent image must be configured to run as **UID 61100**. This UID is shared diff --git a/gemini/Dockerfile b/gemini/Dockerfile index fbe29c2ce..1a46343ce 100644 --- a/gemini/Dockerfile +++ b/gemini/Dockerfile @@ -27,11 +27,15 @@ RUN ARCH=$(dpkg --print-architecture) \ ENV PATH="/usr/local/go/bin:${PATH}" +ARG KANON_VERSION=3edde4571b0aad2530627d94c8d2624ab44a3952 +RUN GOBIN=/usr/local/bin go install github.com/kelos-dev/kanon@${KANON_VERSION} + ARG GEMINI_CLI_VERSION=0.45.1 RUN npm install -g @google/gemini-cli@${GEMINI_CLI_VERSION} COPY gemini/kelos_entrypoint.sh /kelos_entrypoint.sh -RUN chmod +x /kelos_entrypoint.sh +COPY agent-images/kanon_env.sh /kelos/kanon_env.sh +RUN chmod +x /kelos_entrypoint.sh /kelos/kanon_env.sh ARG TARGETARCH COPY bin/kelos-capture-linux-${TARGETARCH} /kelos/kelos-capture diff --git a/internal/controller/job_builder.go b/internal/controller/job_builder.go index e117060cd..4f5f1790c 100644 --- a/internal/controller/job_builder.go +++ b/internal/controller/job_builder.go @@ -60,6 +60,10 @@ const ( // PluginMountPath is the mount path for the plugin volume. PluginMountPath = "/kelos/plugin" + // KanonInstructionsPath is the path inside the generated Kanon home that + // contains AgentConfig instructions. + KanonInstructionsPath = "instructions/kelos.md" + // NodeImage is the image used for running Node.js-based init containers // (e.g., installing skills.sh packages). NodeImage = "node:22.14.0-alpine" @@ -532,6 +536,17 @@ func (b *JobBuilder) buildAgentJob(task *kelosv1alpha1.Task, workspace *kelosv1a }) } + if agentConfig.AgentsMD != "" || len(agentConfig.MCPServers) > 0 { + kanonConfig, err := buildKanonConfigJSON(agentConfig) + if err != nil { + return nil, fmt.Errorf("invalid Kanon configuration: %w", err) + } + mainContainer.Env = append(mainContainer.Env, corev1.EnvVar{ + Name: "KELOS_KANON_CONFIG", + Value: kanonConfig, + }) + } + needsPluginVolume := len(agentConfig.Plugins) > 0 || len(agentConfig.Skills) > 0 if needsPluginVolume { volumes = append(volumes, corev1.Volume{ @@ -1003,19 +1018,17 @@ type mcpServerJSON struct { Env map[string]string `json:"env,omitempty"` } -// buildMCPServersJSON converts MCPServerSpec entries into a JSON string -// that matches the .mcp.json format: {"mcpServers":{"name":{...},...}}. -func buildMCPServersJSON(servers []kelosv1alpha1.MCPServerSpec) (string, error) { +func buildMCPServerMap(servers []kelosv1alpha1.MCPServerSpec) (map[string]mcpServerJSON, error) { mcpMap := make(map[string]mcpServerJSON, len(servers)) for _, s := range servers { if s.Name == "" { - return "", fmt.Errorf("MCP server name is empty") + return nil, fmt.Errorf("MCP server name is empty") } if err := sanitizeComponentName(s.Name, "MCP server"); err != nil { - return "", err + return nil, err } if _, exists := mcpMap[s.Name]; exists { - return "", fmt.Errorf("duplicate MCP server name %q", s.Name) + return nil, fmt.Errorf("duplicate MCP server name %q", s.Name) } entry := mcpServerJSON{ Type: s.Type, @@ -1027,6 +1040,16 @@ func buildMCPServersJSON(servers []kelosv1alpha1.MCPServerSpec) (string, error) } mcpMap[s.Name] = entry } + return mcpMap, nil +} + +// buildMCPServersJSON converts MCPServerSpec entries into a JSON string +// that matches the .mcp.json format: {"mcpServers":{"name":{...},...}}. +func buildMCPServersJSON(servers []kelosv1alpha1.MCPServerSpec) (string, error) { + mcpMap, err := buildMCPServerMap(servers) + if err != nil { + return "", err + } wrapper := struct { MCPServers map[string]mcpServerJSON `json:"mcpServers"` }{MCPServers: mcpMap} @@ -1036,3 +1059,40 @@ func buildMCPServersJSON(servers []kelosv1alpha1.MCPServerSpec) (string, error) } return string(data), nil } + +// buildKanonConfigJSON converts the AgentConfig fields Kanon supports today +// into a Kanon source config. Legacy KELOS_* env vars remain populated for +// plugins and for agents that do not yet have Kanon adapters. +func buildKanonConfigJSON(agentConfig *kelosv1alpha1.AgentConfigSpec) (string, error) { + type kanonInstructionsJSON struct { + Files []string `json:"files,omitempty"` + } + type kanonMCPJSON struct { + Servers map[string]mcpServerJSON `json:"servers,omitempty"` + } + type kanonConfigJSON struct { + Version int `json:"version"` + Instructions *kanonInstructionsJSON `json:"instructions,omitempty"` + MCP *kanonMCPJSON `json:"mcp,omitempty"` + } + + cfg := kanonConfigJSON{Version: 1} + if agentConfig.AgentsMD != "" { + cfg.Instructions = &kanonInstructionsJSON{ + Files: []string{KanonInstructionsPath}, + } + } + if len(agentConfig.MCPServers) > 0 { + mcpMap, err := buildMCPServerMap(agentConfig.MCPServers) + if err != nil { + return "", err + } + cfg.MCP = &kanonMCPJSON{Servers: mcpMap} + } + + data, err := json.Marshal(cfg) + if err != nil { + return "", fmt.Errorf("marshalling Kanon config: %w", err) + } + return string(data), nil +} diff --git a/internal/controller/job_builder_test.go b/internal/controller/job_builder_test.go index dc7be6834..61d2b6ec3 100644 --- a/internal/controller/job_builder_test.go +++ b/internal/controller/job_builder_test.go @@ -3598,6 +3598,115 @@ func TestBuildJob_AgentConfigMCPServers(t *testing.T) { } } +func TestBuildJob_AgentConfigKanonConfig(t *testing.T) { + builder := NewJobBuilder() + task := &kelosv1alpha1.Task{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-kanon-config", + Namespace: "default", + }, + Spec: kelosv1alpha1.TaskSpec{ + Type: AgentTypeCodex, + Prompt: "Fix issue", + Credentials: kelosv1alpha1.Credentials{ + Type: kelosv1alpha1.CredentialTypeAPIKey, + SecretRef: &kelosv1alpha1.SecretReference{Name: "my-secret"}, + }, + }, + } + + agentConfig := &kelosv1alpha1.AgentConfigSpec{ + AgentsMD: "Follow TDD", + Plugins: []kelosv1alpha1.PluginSpec{ + { + Name: "team-tools", + Skills: []kelosv1alpha1.SkillDefinition{ + {Name: "deploy", Content: "Deploy instructions"}, + }, + }, + }, + MCPServers: []kelosv1alpha1.MCPServerSpec{ + { + Name: "local-db", + Type: "stdio", + Command: "npx", + Args: []string{"-y", "@bytebase/dbhub"}, + Env: map[string]string{"DSN": "postgres://localhost/db"}, + }, + }, + } + + job, err := builder.Build(task, nil, agentConfig, task.Spec.Prompt) + if err != nil { + t.Fatalf("Build() returned error: %v", err) + } + + envMap := map[string]string{} + for _, env := range job.Spec.Template.Spec.Containers[0].Env { + if env.Value != "" { + envMap[env.Name] = env.Value + } + } + + if envMap["KELOS_AGENTS_MD"] != "Follow TDD" { + t.Errorf("Expected KELOS_AGENTS_MD=%q, got %q", "Follow TDD", envMap["KELOS_AGENTS_MD"]) + } + if envMap["KELOS_PLUGIN_DIR"] != PluginMountPath { + t.Errorf("Expected KELOS_PLUGIN_DIR=%q, got %q", PluginMountPath, envMap["KELOS_PLUGIN_DIR"]) + } + if envMap["KELOS_MCP_SERVERS"] == "" { + t.Fatal("Expected KELOS_MCP_SERVERS to be set") + } + if envMap["KELOS_KANON_CONFIG"] == "" { + t.Fatal("Expected KELOS_KANON_CONFIG to be set") + } + + var parsed struct { + Version int `json:"version"` + Instructions struct { + Files []string `json:"files"` + } `json:"instructions"` + MCP struct { + Servers map[string]struct { + Type string `json:"type"` + Command string `json:"command"` + Args []string `json:"args"` + Env map[string]string `json:"env"` + } `json:"servers"` + } `json:"mcp"` + Skills []any `json:"skills"` + } + if err := json.Unmarshal([]byte(envMap["KELOS_KANON_CONFIG"]), &parsed); err != nil { + t.Fatalf("Failed to parse KELOS_KANON_CONFIG JSON: %v", err) + } + + if parsed.Version != 1 { + t.Errorf("Expected Kanon version 1, got %d", parsed.Version) + } + if len(parsed.Instructions.Files) != 1 || parsed.Instructions.Files[0] != KanonInstructionsPath { + t.Errorf("Expected Kanon instructions file %q, got %v", KanonInstructionsPath, parsed.Instructions.Files) + } + localDB, ok := parsed.MCP.Servers["local-db"] + if !ok { + t.Fatal("Expected local-db MCP server in Kanon config") + } + if localDB.Type != "stdio" { + t.Errorf("Expected local-db type stdio, got %q", localDB.Type) + } + if localDB.Command != "npx" { + t.Errorf("Expected local-db command npx, got %q", localDB.Command) + } + if len(localDB.Args) != 2 || localDB.Args[0] != "-y" || localDB.Args[1] != "@bytebase/dbhub" { + t.Errorf("Expected local-db args [-y @bytebase/dbhub], got %v", localDB.Args) + } + if localDB.Env["DSN"] != "postgres://localhost/db" { + t.Errorf("Expected local-db env DSN, got %v", localDB.Env) + } + if len(parsed.Skills) != 0 { + t.Errorf("Expected Kanon config to omit plugin skills, got %v", parsed.Skills) + } +} + func TestBuildJob_AgentConfigMCPServersWithHTTPHeaders(t *testing.T) { builder := NewJobBuilder() task := &kelosv1alpha1.Task{ @@ -3710,7 +3819,7 @@ func TestBuildJob_AgentConfigMCPServersWithPluginsAndAgentsMD(t *testing.T) { } } - // All three should be set: KELOS_AGENTS_MD, KELOS_PLUGIN_DIR, KELOS_MCP_SERVERS. + // Existing env vars should still be set alongside KELOS_KANON_CONFIG. if envMap["KELOS_AGENTS_MD"] != "Follow TDD" { t.Errorf("Expected KELOS_AGENTS_MD=%q, got %q", "Follow TDD", envMap["KELOS_AGENTS_MD"]) } @@ -3720,6 +3829,9 @@ func TestBuildJob_AgentConfigMCPServersWithPluginsAndAgentsMD(t *testing.T) { if envMap["KELOS_MCP_SERVERS"] == "" { t.Error("Expected KELOS_MCP_SERVERS to be set") } + if envMap["KELOS_KANON_CONFIG"] == "" { + t.Error("Expected KELOS_KANON_CONFIG to be set") + } // Should have plugin volume and init container. if len(job.Spec.Template.Spec.Volumes) != 1 { diff --git a/opencode/Dockerfile b/opencode/Dockerfile index 7f46d89de..edf7df76f 100644 --- a/opencode/Dockerfile +++ b/opencode/Dockerfile @@ -27,11 +27,15 @@ RUN ARCH=$(dpkg --print-architecture) \ ENV PATH="/usr/local/go/bin:${PATH}" +ARG KANON_VERSION=3edde4571b0aad2530627d94c8d2624ab44a3952 +RUN GOBIN=/usr/local/bin go install github.com/kelos-dev/kanon@${KANON_VERSION} + ARG OPENCODE_VERSION=1.15.13 RUN npm install -g opencode-ai@${OPENCODE_VERSION} COPY opencode/kelos_entrypoint.sh /kelos_entrypoint.sh -RUN chmod +x /kelos_entrypoint.sh +COPY agent-images/kanon_env.sh /kelos/kanon_env.sh +RUN chmod +x /kelos_entrypoint.sh /kelos/kanon_env.sh ARG TARGETARCH COPY bin/kelos-capture-linux-${TARGETARCH} /kelos/kelos-capture