From 7c453e1fd4b5554b7ed78800b069889e8620df0e Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:12:00 +0100 Subject: [PATCH 01/11] Refactor code to find env vars for secrets Signed-off-by: David Gageot --- cmd/root/build.go | 1 + pkg/secrets/gather.go | 124 +++++++++++++++++++++++++++++++++++ pkg/teamloader/teamloader.go | 77 ++-------------------- 3 files changed, 130 insertions(+), 72 deletions(-) create mode 100644 pkg/secrets/gather.go diff --git a/cmd/root/build.go b/cmd/root/build.go index 22b607f88..63091811a 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -20,6 +20,7 @@ var push bool func NewBuildCmd() *cobra.Command { cmd := &cobra.Command{ Use: "build ", + Short: "Build a Docker image for the agent", Args: cobra.ExactArgs(2), RunE: runBuildCommand, Hidden: true, diff --git a/pkg/secrets/gather.go b/pkg/secrets/gather.go new file mode 100644 index 000000000..ad74a2daf --- /dev/null +++ b/pkg/secrets/gather.go @@ -0,0 +1,124 @@ +package secrets + +import ( + "context" + "fmt" + "sort" + "strings" + + "github.com/docker/cagent/pkg/config" + latest "github.com/docker/cagent/pkg/config/v2" + "github.com/docker/cagent/pkg/environment" + "github.com/docker/cagent/pkg/gateway" + "github.com/docker/cagent/pkg/model/provider" +) + +// GatherMissingEnvVars finds out which environment variables are required by the models and tools. +// This allows exiting early with a proper error message instead of failing later when trying to use a model or tool. +// TODO(dga): This code contains lots of duplication and ought to be refactored. +func GatherMissingEnvVars(ctx context.Context, cfg *latest.Config, env environment.Provider, runtimeConfig config.RuntimeConfig) ([]string, error) { + requiredEnv := map[string]bool{} + + // Models + if runtimeConfig.ModelsGateway == "" { + names := GatherEnvVarsForModels(ctx, cfg) + for _, e := range names { + requiredEnv[e] = true + } + } + + // Tools + if runtimeConfig.ToolsGateway == "" && !environment.IsInContainer() { + names, err := GatherEnvVarsForTools(ctx, cfg) + if err != nil { + return nil, err + } + for _, e := range names { + requiredEnv[e] = true + } + } + + // Check for missing + var missing []string + for _, e := range mcpToSortedList(requiredEnv) { + if env.Get(ctx, e) == "" { + missing = append(missing, e) + } + } + + return missing, nil +} + +func GatherEnvVarsForModels(ctx context.Context, cfg *latest.Config) []string { + requiredEnv := map[string]bool{} + + for name := range cfg.Models { + model := cfg.Models[name] + + // Use the token environment variable from the alias if available + if alias, exists := provider.ProviderAliases[model.Provider]; exists { + if alias.TokenEnvVar != "" { + requiredEnv[alias.TokenEnvVar] = true + } + } else { + // Fallback to hardcoded mappings for unknown providers + switch model.Provider { + case "openai": + requiredEnv["OPENAI_API_KEY"] = true + case "anthropic": + requiredEnv["ANTHROPIC_API_KEY"] = true + case "google": + requiredEnv["GOOGLE_API_KEY"] = true + } + } + } + + for _, agent := range cfg.Agents { + model := agent.Model + switch { + case strings.HasPrefix(model, "openai/"): + requiredEnv["OPENAI_API_KEY"] = true + case strings.HasPrefix(model, "anthropic/"): + requiredEnv["ANTHROPIC_API_KEY"] = true + case strings.HasPrefix(model, "google/"): + requiredEnv["GOOGLE_API_KEY"] = true + } + } + + return mcpToSortedList(requiredEnv) +} + +func GatherEnvVarsForTools(ctx context.Context, cfg *latest.Config) ([]string, error) { + requiredEnv := map[string]bool{} + + for _, agent := range cfg.Agents { + for i := range agent.Toolsets { + toolSet := agent.Toolsets[i] + + if toolSet.Type == "mcp" && toolSet.Ref != "" { + mcpServerName := gateway.ParseServerRef(toolSet.Ref) + + secrets, err := gateway.RequiredEnvVars(ctx, mcpServerName, gateway.DockerCatalogURL) + if err != nil { + return nil, fmt.Errorf("reading which secrets the MCP server needs: %w", err) + } + for _, secret := range secrets { + requiredEnv[secret.Env] = true + } + } + } + } + + return mcpToSortedList(requiredEnv), nil +} + +func mcpToSortedList(requiredEnv map[string]bool) []string { + var requiredEnvList []string + + for e := range requiredEnv { + requiredEnvList = append(requiredEnvList, e) + } + sort.Strings(requiredEnvList) + + return requiredEnvList +} diff --git a/pkg/teamloader/teamloader.go b/pkg/teamloader/teamloader.go index 88ec9bacc..f58f595a3 100644 --- a/pkg/teamloader/teamloader.go +++ b/pkg/teamloader/teamloader.go @@ -7,18 +7,17 @@ import ( "log/slog" "os" "path/filepath" - "sort" "strings" "github.com/docker/cagent/pkg/agent" "github.com/docker/cagent/pkg/config" latest "github.com/docker/cagent/pkg/config/v2" "github.com/docker/cagent/pkg/environment" - "github.com/docker/cagent/pkg/gateway" "github.com/docker/cagent/pkg/memory" "github.com/docker/cagent/pkg/memory/database/sqlite" "github.com/docker/cagent/pkg/model/provider" "github.com/docker/cagent/pkg/model/provider/options" + "github.com/docker/cagent/pkg/secrets" "github.com/docker/cagent/pkg/team" "github.com/docker/cagent/pkg/tools" "github.com/docker/cagent/pkg/tools/builtin" @@ -79,84 +78,18 @@ func FindAgentPaths(agentsPathOrDirectory string) ([]string, error) { // checkRequiredEnvVars checks which environment variables are required by the models and tools. // This allows exiting early with a proper error message instead of failing later when trying to use a model or tool. -// TODO(dga): This code contains lots of duplication and ought to be refactored. func checkRequiredEnvVars(ctx context.Context, cfg *latest.Config, env environment.Provider, runtimeConfig config.RuntimeConfig) error { - requiredEnv := map[string]bool{} - - // Models - if runtimeConfig.ModelsGateway == "" { - for name := range cfg.Models { - model := cfg.Models[name] - // Use the token environment variable from the alias if available - if alias, exists := provider.ProviderAliases[model.Provider]; exists { - if alias.TokenEnvVar != "" { - requiredEnv[alias.TokenEnvVar] = true - } - } else { - // Fallback to hardcoded mappings for unknown providers - switch model.Provider { - case "openai": - requiredEnv["OPENAI_API_KEY"] = true - case "anthropic": - requiredEnv["ANTHROPIC_API_KEY"] = true - case "google": - requiredEnv["GOOGLE_API_KEY"] = true - } - } - } - - for _, agent := range cfg.Agents { - model := agent.Model - switch { - case strings.HasPrefix(model, "openai/"): - requiredEnv["OPENAI_API_KEY"] = true - case strings.HasPrefix(model, "anthropic/"): - requiredEnv["ANTHROPIC_API_KEY"] = true - case strings.HasPrefix(model, "google/"): - requiredEnv["GOOGLE_API_KEY"] = true - } - } - } - - // Tools - if runtimeConfig.ToolsGateway == "" && !environment.IsInContainer() { - for _, agent := range cfg.Agents { - for i := range agent.Toolsets { - toolSet := agent.Toolsets[i] - - if toolSet.Type == "mcp" && toolSet.Ref != "" { - mcpServerName := gateway.ParseServerRef(toolSet.Ref) - - secrets, err := gateway.RequiredEnvVars(ctx, mcpServerName, gateway.DockerCatalogURL) - if err != nil { - return fmt.Errorf("reading which secrets the MCP server needs: %w", err) - } - for _, secret := range secrets { - requiredEnv[secret.Env] = true - } - } - } - } + requiredEnv, err := secrets.GatherMissingEnvVars(ctx, cfg, env, runtimeConfig) + if err != nil { + return fmt.Errorf("gathering required environment variables: %w", err) } if len(requiredEnv) == 0 { return nil } - var requiredEnvList []string - for e := range requiredEnv { - if env.Get(ctx, e) == "" { - requiredEnvList = append(requiredEnvList, e) - } - } - - if len(requiredEnvList) == 0 { - return nil - } - - sort.Strings(requiredEnvList) return &environment.RequiredEnvError{ - Missing: requiredEnvList, + Missing: requiredEnv, } } From e9be9eb511d432ea7b4a2d1a3d161941846989d5 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:21:24 +0100 Subject: [PATCH 02/11] Remove duplication Signed-off-by: David Gageot --- cmd/root/build.go | 47 ++----------------------------------------- pkg/secrets/gather.go | 4 ++-- 2 files changed, 4 insertions(+), 47 deletions(-) diff --git a/cmd/root/build.go b/cmd/root/build.go index 63091811a..0c5531140 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -12,7 +12,7 @@ import ( "github.com/docker/cagent/internal/telemetry" "github.com/docker/cagent/pkg/config" latest "github.com/docker/cagent/pkg/config/v2" - "github.com/docker/cagent/pkg/model/provider" + "github.com/docker/cagent/pkg/secrets" ) var push bool @@ -42,7 +42,7 @@ func runBuildCommand(cmd *cobra.Command, args []string) error { return err } - secrets := gatherRequiredEnv(cfg) + secrets := secrets.GatherEnvVarsForModels(cfg) mcpServers := gatherMCPServers(cfg) tmp, err := os.MkdirTemp("", "build") @@ -96,49 +96,6 @@ LABEL com.docker.agent.secrets="%s" return buildCmd.Run() } -func gatherRequiredEnv(cfg *latest.Config) []string { - requiredEnv := map[string]bool{} - - for name := range cfg.Models { - model := cfg.Models[name] - // Use the token environment variable from the alias if available - if alias, exists := provider.ProviderAliases[model.Provider]; exists { - if alias.TokenEnvVar != "" { - requiredEnv[alias.TokenEnvVar] = true - } - } else { - // Fallback to hardcoded mappings for unknown providers - switch model.Provider { - case "openai": - requiredEnv["OPENAI_API_KEY"] = true - case "anthropic": - requiredEnv["ANTHROPIC_API_KEY"] = true - case "google": - requiredEnv["GOOGLE_API_KEY"] = true - } - } - } - - for _, agent := range cfg.Agents { - model := agent.Model - switch { - case strings.HasPrefix(model, "openai/"): - requiredEnv["OPENAI_API_KEY"] = true - case strings.HasPrefix(model, "anthropic/"): - requiredEnv["ANTHROPIC_API_KEY"] = true - case strings.HasPrefix(model, "google/"): - requiredEnv["GOOGLE_API_KEY"] = true - } - } - - var requiredEnvList []string - for e := range requiredEnv { - requiredEnvList = append(requiredEnvList, e) - } - - return requiredEnvList -} - func gatherMCPServers(cfg *latest.Config) []string { requiredServers := map[string]bool{} diff --git a/pkg/secrets/gather.go b/pkg/secrets/gather.go index ad74a2daf..e10ae06e0 100644 --- a/pkg/secrets/gather.go +++ b/pkg/secrets/gather.go @@ -21,7 +21,7 @@ func GatherMissingEnvVars(ctx context.Context, cfg *latest.Config, env environme // Models if runtimeConfig.ModelsGateway == "" { - names := GatherEnvVarsForModels(ctx, cfg) + names := GatherEnvVarsForModels(cfg) for _, e := range names { requiredEnv[e] = true } @@ -49,7 +49,7 @@ func GatherMissingEnvVars(ctx context.Context, cfg *latest.Config, env environme return missing, nil } -func GatherEnvVarsForModels(ctx context.Context, cfg *latest.Config) []string { +func GatherEnvVarsForModels(cfg *latest.Config) []string { requiredEnv := map[string]bool{} for name := range cfg.Models { From bb722b45ef224f61df0dc0584763807a1e80df6f Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:23:00 +0100 Subject: [PATCH 03/11] Add missing case for aliases models Signed-off-by: David Gageot --- pkg/secrets/gather.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/secrets/gather.go b/pkg/secrets/gather.go index e10ae06e0..fb2455f49 100644 --- a/pkg/secrets/gather.go +++ b/pkg/secrets/gather.go @@ -75,6 +75,13 @@ func GatherEnvVarsForModels(cfg *latest.Config) []string { for _, agent := range cfg.Agents { model := agent.Model + + for prefix, alias := range provider.ProviderAliases { + if strings.HasPrefix(model, prefix+"/") && alias.TokenEnvVar != "" { + requiredEnv[alias.TokenEnvVar] = true + } + } + switch { case strings.HasPrefix(model, "openai/"): requiredEnv["OPENAI_API_KEY"] = true From 89714acc1d0244cf027fbb59f434377f1c2b7cc6 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:26:04 +0100 Subject: [PATCH 04/11] Remove duplication Signed-off-by: David Gageot --- cmd/root/build.go | 24 +----------------------- pkg/secrets/gather.go | 25 ++++++++++--------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/cmd/root/build.go b/cmd/root/build.go index 0c5531140..d4da51fea 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -11,7 +11,6 @@ import ( "github.com/docker/cagent/internal/telemetry" "github.com/docker/cagent/pkg/config" - latest "github.com/docker/cagent/pkg/config/v2" "github.com/docker/cagent/pkg/secrets" ) @@ -43,7 +42,7 @@ func runBuildCommand(cmd *cobra.Command, args []string) error { } secrets := secrets.GatherEnvVarsForModels(cfg) - mcpServers := gatherMCPServers(cfg) + mcpServers := config.GatherMCPServerReferences(cfg) tmp, err := os.MkdirTemp("", "build") if err != nil { @@ -95,24 +94,3 @@ LABEL com.docker.agent.secrets="%s" return buildCmd.Run() } - -func gatherMCPServers(cfg *latest.Config) []string { - requiredServers := map[string]bool{} - - for _, agent := range cfg.Agents { - for i := range agent.Toolsets { - toolSet := agent.Toolsets[i] - - if toolSet.Type == "mcp" && toolSet.Ref != "" { - requiredServers[toolSet.Ref] = true - } - } - } - - var requiredServersList []string - for e := range requiredServers { - requiredServersList = append(requiredServersList, e) - } - - return requiredServersList -} diff --git a/pkg/secrets/gather.go b/pkg/secrets/gather.go index fb2455f49..cbf7c20e1 100644 --- a/pkg/secrets/gather.go +++ b/pkg/secrets/gather.go @@ -98,21 +98,16 @@ func GatherEnvVarsForModels(cfg *latest.Config) []string { func GatherEnvVarsForTools(ctx context.Context, cfg *latest.Config) ([]string, error) { requiredEnv := map[string]bool{} - for _, agent := range cfg.Agents { - for i := range agent.Toolsets { - toolSet := agent.Toolsets[i] - - if toolSet.Type == "mcp" && toolSet.Ref != "" { - mcpServerName := gateway.ParseServerRef(toolSet.Ref) - - secrets, err := gateway.RequiredEnvVars(ctx, mcpServerName, gateway.DockerCatalogURL) - if err != nil { - return nil, fmt.Errorf("reading which secrets the MCP server needs: %w", err) - } - for _, secret := range secrets { - requiredEnv[secret.Env] = true - } - } + for _, ref := range config.GatherMCPServerReferences(cfg) { + mcpServerName := gateway.ParseServerRef(ref) + + secrets, err := gateway.RequiredEnvVars(ctx, mcpServerName, gateway.DockerCatalogURL) + if err != nil { + return nil, fmt.Errorf("reading which secrets the MCP server needs: %w", err) + } + + for _, secret := range secrets { + requiredEnv[secret.Env] = true } } From cdd59d16ccc03f8d111210bdab2e5c6d6f09bc4d Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:27:03 +0100 Subject: [PATCH 05/11] Add a TODO Signed-off-by: David Gageot --- pkg/config/mcp.go | 29 +++++++++++++++++++++++++++++ pkg/gateway/catalog.go | 1 + 2 files changed, 30 insertions(+) create mode 100644 pkg/config/mcp.go diff --git a/pkg/config/mcp.go b/pkg/config/mcp.go new file mode 100644 index 000000000..981bbd4d1 --- /dev/null +++ b/pkg/config/mcp.go @@ -0,0 +1,29 @@ +package config + +import ( + "sort" + + latest "github.com/docker/cagent/pkg/config/v2" +) + +func GatherMCPServerReferences(cfg *latest.Config) []string { + servers := map[string]bool{} + + for _, agent := range cfg.Agents { + for i := range agent.Toolsets { + toolSet := agent.Toolsets[i] + + if toolSet.Type == "mcp" && toolSet.Ref != "" { + servers[toolSet.Ref] = true + } + } + } + + var list []string + for e := range servers { + list = append(list, e) + } + sort.Strings(list) + + return list +} diff --git a/pkg/gateway/catalog.go b/pkg/gateway/catalog.go index 361792b12..a262390a8 100644 --- a/pkg/gateway/catalog.go +++ b/pkg/gateway/catalog.go @@ -30,6 +30,7 @@ func RequiredEnvVars(ctx context.Context, serverName, catalogURL string) ([]Secr return server.Secrets, nil } +// TODO(dga): cache the catalog. func readCatalog(ctx context.Context, url string) (Catalog, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { From 428278ad0508a96cb67468f940b75fa4529b07b8 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:29:45 +0100 Subject: [PATCH 06/11] Make the image name optional Signed-off-by: David Gageot --- cmd/root/build.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/root/build.go b/cmd/root/build.go index d4da51fea..8bdb3da5e 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -18,9 +18,9 @@ var push bool func NewBuildCmd() *cobra.Command { cmd := &cobra.Command{ - Use: "build ", + Use: "build [docker-image-name]", Short: "Build a Docker image for the agent", - Args: cobra.ExactArgs(2), + Args: cobra.MinimumNArgs(1), RunE: runBuildCommand, Hidden: true, } @@ -83,7 +83,10 @@ LABEL com.docker.agent.secrets="%s" return err } - buildArgs := []string{"build", "-t", args[1]} + buildArgs := []string{"build"} + if len(args) > 1 { + buildArgs = append(buildArgs, "-t", args[1]) + } if push { buildArgs = append(buildArgs, "--push") } From f20f3868e58c5b1e00d3163aad5cbd77d23c9e2b Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:40:11 +0100 Subject: [PATCH 07/11] Simpler cagent build Signed-off-by: David Gageot --- cmd/root/build.go | 43 +++++++++++++++++-------------------------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/cmd/root/build.go b/cmd/root/build.go index 8bdb3da5e..482958eb4 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -33,32 +33,34 @@ func NewBuildCmd() *cobra.Command { func runBuildCommand(cmd *cobra.Command, args []string) error { telemetry.TrackCommand("build", args) - fileName := filepath.Base(args[0]) - parentDir := filepath.Dir(args[0]) + agentFilePath := args[0] + agentYaml, err := os.ReadFile(agentFilePath) + if err != nil { + return err + } + fileName := filepath.Base(agentFilePath) + parentDir := filepath.Dir(agentFilePath) cfg, err := config.LoadConfigSecure(fileName, parentDir) if err != nil { return err } + // Analyze the config to find which secrets are needed secrets := secrets.GatherEnvVarsForModels(cfg) mcpServers := config.GatherMCPServerReferences(cfg) - tmp, err := os.MkdirTemp("", "build") - if err != nil { - return err - } - defer os.RemoveAll(tmp) - // TODO(dga): set the right entrypoint. - err = os.WriteFile(filepath.Join(tmp, "Dockerfile"), fmt.Appendf(nil, `# syntax=docker/dockerfile:1 + dockerfile := fmt.Sprintf(`# syntax=docker/dockerfile:1 FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 RUN adduser -D cagent ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent RUN chmod +x /cagent -COPY agent.yaml / -RUN chmod 666 /agent.yaml +RUN cat < /agent.yaml +%s +EOF +RUN chmod +r /agent.yaml USER cagent ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"] @@ -68,20 +70,7 @@ LABEL org.opencontainers.image.description="%s" LABEL org.opencontainers.image.licenses="%s" LABEL com.docker.agent.mcp-servers="%s" LABEL com.docker.agent.secrets="%s" -`, cfg.Agents["root"].Description, cfg.Metadata.License, strings.Join(mcpServers, ","), strings.Join(secrets, ",")), 0o700) - if err != nil { - return err - } - - agentYaml, err := os.ReadFile(args[0]) - if err != nil { - return err - } - - err = os.WriteFile(filepath.Join(tmp, "agent.yaml"), agentYaml, 0o700) - if err != nil { - return err - } +`, string(agentYaml), cfg.Agents["root"].Description, cfg.Metadata.License, strings.Join(mcpServers, ","), strings.Join(secrets, ",")) buildArgs := []string{"build"} if len(args) > 1 { @@ -90,8 +79,10 @@ LABEL com.docker.agent.secrets="%s" if push { buildArgs = append(buildArgs, "--push") } - buildArgs = append(buildArgs, tmp) + buildArgs = append(buildArgs, "-") + buildCmd := exec.CommandContext(cmd.Context(), "docker", buildArgs...) + buildCmd.Stdin = strings.NewReader(dockerfile) buildCmd.Stdout = os.Stdout buildCmd.Stderr = os.Stderr From bcd4a2e4ec1f78fb3ff7a06342c0288a73979b08 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:44:48 +0100 Subject: [PATCH 08/11] Move code where it belongs Signed-off-by: David Gageot --- cmd/root/build.go | 62 ++----------------- pkg/oci/build.go | 69 ++++++++++++++++++++++ pkg/oci/{build_test.go => package_test.go} | 0 3 files changed, 73 insertions(+), 58 deletions(-) create mode 100644 pkg/oci/build.go rename pkg/oci/{build_test.go => package_test.go} (100%) diff --git a/cmd/root/build.go b/cmd/root/build.go index 482958eb4..c8d9f933f 100644 --- a/cmd/root/build.go +++ b/cmd/root/build.go @@ -1,17 +1,10 @@ package root import ( - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "github.com/spf13/cobra" "github.com/docker/cagent/internal/telemetry" - "github.com/docker/cagent/pkg/config" - "github.com/docker/cagent/pkg/secrets" + "github.com/docker/cagent/pkg/oci" ) var push bool @@ -34,57 +27,10 @@ func runBuildCommand(cmd *cobra.Command, args []string) error { telemetry.TrackCommand("build", args) agentFilePath := args[0] - agentYaml, err := os.ReadFile(agentFilePath) - if err != nil { - return err - } - - fileName := filepath.Base(agentFilePath) - parentDir := filepath.Dir(agentFilePath) - cfg, err := config.LoadConfigSecure(fileName, parentDir) - if err != nil { - return err - } - - // Analyze the config to find which secrets are needed - secrets := secrets.GatherEnvVarsForModels(cfg) - mcpServers := config.GatherMCPServerReferences(cfg) - - // TODO(dga): set the right entrypoint. - dockerfile := fmt.Sprintf(`# syntax=docker/dockerfile:1 -FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 - -RUN adduser -D cagent -ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent -RUN chmod +x /cagent -RUN cat < /agent.yaml -%s -EOF -RUN chmod +r /agent.yaml -USER cagent -ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"] - -LABEL com.docker.agent.packaging.version="v0.0.1" -LABEL com.docker.agent.runtime="cagent" -LABEL org.opencontainers.image.description="%s" -LABEL org.opencontainers.image.licenses="%s" -LABEL com.docker.agent.mcp-servers="%s" -LABEL com.docker.agent.secrets="%s" -`, string(agentYaml), cfg.Agents["root"].Description, cfg.Metadata.License, strings.Join(mcpServers, ","), strings.Join(secrets, ",")) - - buildArgs := []string{"build"} + dockerImageName := "" if len(args) > 1 { - buildArgs = append(buildArgs, "-t", args[1]) + dockerImageName = args[1] } - if push { - buildArgs = append(buildArgs, "--push") - } - buildArgs = append(buildArgs, "-") - - buildCmd := exec.CommandContext(cmd.Context(), "docker", buildArgs...) - buildCmd.Stdin = strings.NewReader(dockerfile) - buildCmd.Stdout = os.Stdout - buildCmd.Stderr = os.Stderr - return buildCmd.Run() + return oci.BuildDockerImage(cmd.Context(), agentFilePath, dockerImageName, push) } diff --git a/pkg/oci/build.go b/pkg/oci/build.go new file mode 100644 index 000000000..a58837726 --- /dev/null +++ b/pkg/oci/build.go @@ -0,0 +1,69 @@ +package oci + +import ( + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/docker/cagent/pkg/config" + "github.com/docker/cagent/pkg/secrets" +) + +func BuildDockerImage(ctx context.Context, agentFilePath, dockerImageName string, push bool) error { + agentYaml, err := os.ReadFile(agentFilePath) + if err != nil { + return err + } + + fileName := filepath.Base(agentFilePath) + parentDir := filepath.Dir(agentFilePath) + cfg, err := config.LoadConfigSecure(fileName, parentDir) + if err != nil { + return err + } + + // Analyze the config to find which secrets are needed + secrets := secrets.GatherEnvVarsForModels(cfg) + mcpServers := config.GatherMCPServerReferences(cfg) + + // TODO(dga): set the right entrypoint. + dockerfile := fmt.Sprintf(`# syntax=docker/dockerfile:1 +FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 + +RUN adduser -D cagent +ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent +RUN chmod +x /cagent +RUN cat < /agent.yaml +%s +EOF +RUN chmod +r /agent.yaml +USER cagent +ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"] + +LABEL com.docker.agent.packaging.version="v0.0.1" +LABEL com.docker.agent.runtime="cagent" +LABEL org.opencontainers.image.description="%s" +LABEL org.opencontainers.image.licenses="%s" +LABEL com.docker.agent.mcp-servers="%s" +LABEL com.docker.agent.secrets="%s" +`, string(agentYaml), cfg.Agents["root"].Description, cfg.Metadata.License, strings.Join(mcpServers, ","), strings.Join(secrets, ",")) + + buildArgs := []string{"build"} + if dockerImageName != "" { + buildArgs = append(buildArgs, "-t", dockerImageName) + if push { + buildArgs = append(buildArgs, "--push") + } + } + buildArgs = append(buildArgs, "-") + + buildCmd := exec.CommandContext(ctx, "docker", buildArgs...) + buildCmd.Stdin = strings.NewReader(dockerfile) + buildCmd.Stdout = os.Stdout + buildCmd.Stderr = os.Stderr + + return buildCmd.Run() +} diff --git a/pkg/oci/build_test.go b/pkg/oci/package_test.go similarity index 100% rename from pkg/oci/build_test.go rename to pkg/oci/package_test.go From 657ba33f4ecaf343016d332c72422f52d926261c Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 09:48:30 +0100 Subject: [PATCH 09/11] Extract Dockerfile template to a file Signed-off-by: David Gageot --- pkg/oci/Dockerfile.template | 19 ++++++++++++++++ pkg/oci/build.go | 45 ++++++++++++++++++++----------------- 2 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 pkg/oci/Dockerfile.template diff --git a/pkg/oci/Dockerfile.template b/pkg/oci/Dockerfile.template new file mode 100644 index 000000000..c7f5835c8 --- /dev/null +++ b/pkg/oci/Dockerfile.template @@ -0,0 +1,19 @@ +# syntax=docker/dockerfile:1 +FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 + +RUN adduser -D cagent +ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent +RUN chmod +x /cagent +RUN cat < /agent.yaml +{{ .AgentConfig }} +EOF +RUN chmod +r /agent.yaml +USER cagent +ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"] + +LABEL com.docker.agent.packaging.version="v0.0.1" +LABEL com.docker.agent.runtime="cagent" +LABEL org.opencontainers.image.description="{{ .Description }}" +LABEL org.opencontainers.image.licenses="{{ .Licenses }}" +LABEL com.docker.agent.mcp-servers="{{ .McpServers }}" +LABEL com.docker.agent.secrets="{{ .Secrets }}" \ No newline at end of file diff --git a/pkg/oci/build.go b/pkg/oci/build.go index a58837726..62fa0a0c7 100644 --- a/pkg/oci/build.go +++ b/pkg/oci/build.go @@ -1,17 +1,23 @@ package oci import ( + "bytes" "context" - "fmt" + _ "embed" + "log/slog" "os" "os/exec" "path/filepath" "strings" + "text/template" "github.com/docker/cagent/pkg/config" "github.com/docker/cagent/pkg/secrets" ) +//go:embed Dockerfile.template +var dockerfileTemplate string + func BuildDockerImage(ctx context.Context, agentFilePath, dockerImageName string, push bool) error { agentYaml, err := os.ReadFile(agentFilePath) if err != nil { @@ -26,31 +32,27 @@ func BuildDockerImage(ctx context.Context, agentFilePath, dockerImageName string } // Analyze the config to find which secrets are needed - secrets := secrets.GatherEnvVarsForModels(cfg) + modelSecrets := secrets.GatherEnvVarsForModels(cfg) mcpServers := config.GatherMCPServerReferences(cfg) - // TODO(dga): set the right entrypoint. - dockerfile := fmt.Sprintf(`# syntax=docker/dockerfile:1 -FROM alpine:3.22@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1 + // Generate the Dockerfile + var dockerfileBuf bytes.Buffer -RUN adduser -D cagent -ADD https://github.com/docker/cagent/releases/download/v1.0.9/cagent-linux-arm64 /cagent -RUN chmod +x /cagent -RUN cat < /agent.yaml -%s -EOF -RUN chmod +r /agent.yaml -USER cagent -ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"] + tpl := template.Must(template.New("Dockerfile").Parse(dockerfileTemplate)) + if err := tpl.Execute(&dockerfileBuf, map[string]any{ + "AgentConfig": string(agentYaml), + "Description": cfg.Agents["root"].Description, + "Licenses": cfg.Metadata.License, + "McpServers": strings.Join(mcpServers, ","), + "Secrets": strings.Join(modelSecrets, ","), + }); err != nil { + return err + } -LABEL com.docker.agent.packaging.version="v0.0.1" -LABEL com.docker.agent.runtime="cagent" -LABEL org.opencontainers.image.description="%s" -LABEL org.opencontainers.image.licenses="%s" -LABEL com.docker.agent.mcp-servers="%s" -LABEL com.docker.agent.secrets="%s" -`, string(agentYaml), cfg.Agents["root"].Description, cfg.Metadata.License, strings.Join(mcpServers, ","), strings.Join(secrets, ",")) + dockerfile := dockerfileBuf.String() + slog.Debug("Generated Dockerfile", "dockerfile", dockerfile) + // Run docker build buildArgs := []string{"build"} if dockerImageName != "" { buildArgs = append(buildArgs, "-t", dockerImageName) @@ -64,6 +66,7 @@ LABEL com.docker.agent.secrets="%s" buildCmd.Stdin = strings.NewReader(dockerfile) buildCmd.Stdout = os.Stdout buildCmd.Stderr = os.Stderr + slog.Debug("running docker build", "args", buildArgs) return buildCmd.Run() } From 6b08fd10fbe07da035c84049e090f58cf132b4d9 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 10:00:35 +0100 Subject: [PATCH 10/11] Fix the entrypoint Signed-off-by: David Gageot --- pkg/oci/Dockerfile.template | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/oci/Dockerfile.template b/pkg/oci/Dockerfile.template index c7f5835c8..aae33cbb3 100644 --- a/pkg/oci/Dockerfile.template +++ b/pkg/oci/Dockerfile.template @@ -7,9 +7,9 @@ RUN chmod +x /cagent RUN cat < /agent.yaml {{ .AgentConfig }} EOF -RUN chmod +r /agent.yaml +RUN chmod +r /agent.yaml && mkdir /data && chmod 777 -R /data USER cagent -ENTRYPOINT ["/cagent", "run", "--debug", "--tui=false", "/agent.yaml", "get my username on github"] +ENTRYPOINT ["/cagent", "api", "--session-db", "/data/session.db", "/agent.yaml"] LABEL com.docker.agent.packaging.version="v0.0.1" LABEL com.docker.agent.runtime="cagent" From aa0cb3c3fcd307d8358c44dc0bfcf186a4cc960b Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 11 Sep 2025 10:01:06 +0100 Subject: [PATCH 11/11] Make the entrypoint more flexible Signed-off-by: David Gageot --- pkg/oci/Dockerfile.template | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/oci/Dockerfile.template b/pkg/oci/Dockerfile.template index aae33cbb3..181d4e3f0 100644 --- a/pkg/oci/Dockerfile.template +++ b/pkg/oci/Dockerfile.template @@ -9,7 +9,8 @@ RUN cat < /agent.yaml EOF RUN chmod +r /agent.yaml && mkdir /data && chmod 777 -R /data USER cagent -ENTRYPOINT ["/cagent", "api", "--session-db", "/data/session.db", "/agent.yaml"] +ENTRYPOINT ["/cagent"] +CMD ["api", "--session-db", "/data/session.db", "/agent.yaml"] LABEL com.docker.agent.packaging.version="v0.0.1" LABEL com.docker.agent.runtime="cagent"