Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions agent-images/kanon_env.sh
Original file line number Diff line number Diff line change
@@ -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
}
6 changes: 5 additions & 1 deletion claude-code/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 25 additions & 15 deletions claude-code/kelos_entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion codex/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 39 additions & 29 deletions codex/kelos_entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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/<plugin>/<skill>/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 = "";
Expand All @@ -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/<plugin>/<skill>/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
Expand Down
6 changes: 5 additions & 1 deletion cursor/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 11 additions & 1 deletion docs/agent-image-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion gemini/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
72 changes: 66 additions & 6 deletions internal/controller/job_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand All @@ -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}
Expand All @@ -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
}
Loading
Loading