Skip to content
Merged
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
7 changes: 7 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ version: "3"

vars:
BINARY_NAME: cagent{{if eq OS "windows"}}.exe{{end}}
CLI_PLUGIN_BINARY_NAME: docker-agent{{if eq OS "windows"}}.exe{{end}}
MAIN_PKG: ./main.go
BUILD_DIR: ./bin
GIT_TAG:
Expand Down Expand Up @@ -35,6 +36,12 @@ tasks:
generates:
- "{{.BUILD_DIR}}/{{.BINARY_NAME}}"

deploy-local:
desc: Deploy the docker agent cli-plugin
deps: ["build"]
cmds:
- cp "{{.BUILD_DIR}}/{{.BINARY_NAME}}" ~/.docker/cli-plugins/{{.CLI_PLUGIN_BINARY_NAME}}

lint:
desc: Run golangci-lint
cmds:
Expand Down
87 changes: 67 additions & 20 deletions cmd/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ import (
"log/slog"
"os"
"path/filepath"
"runtime"
"strings"

"github.com/docker/cli/cli-plugins/metadata"
"github.com/docker/cli/cli-plugins/plugin"
"github.com/docker/cli/cli/command"
"github.com/spf13/cobra"

"github.com/docker/cagent/pkg/environment"
Expand All @@ -28,6 +32,14 @@ type rootFlags struct {
logFile io.Closer
}

func isCliPLugin() bool {
cliPluginBinary := "docker-agent"
if runtime.GOOS == "windows" {
cliPluginBinary += ".exe"
}
return len(os.Args) > 0 && strings.HasSuffix(os.Args[0], cliPluginBinary)
}

func NewRootCmd() *cobra.Command {
var flags rootFlags

Expand Down Expand Up @@ -107,6 +119,14 @@ func NewRootCmd() *cobra.Command {
cmd.AddGroup(&cobra.Group{ID: "advanced", Title: "Advanced Commands:"})
cmd.AddGroup(&cobra.Group{ID: "server", Title: "Server Commands:"})

if isCliPLugin() {
cmd.Use = "agent"
cmd.Short = "create or run AI agents"
cmd.Long = "create or run AI agents"
cmd.Example = ` docker agent run ./agent.yaml
docker agent run agentcatalog/pirate`
}

return cmd
}

Expand Down Expand Up @@ -141,33 +161,60 @@ We collect anonymous usage data to help improve cagent. To disable:
rootCmd.SetOut(stdout)
rootCmd.SetErr(stderr)

if err := rootCmd.ExecuteContext(ctx); err != nil {
if ctx.Err() != nil {
return ctx.Err()
} else if envErr, ok := errors.AsType[*environment.RequiredEnvError](err); ok {
fmt.Fprintln(stderr, "The following environment variables must be set:")
for _, v := range envErr.Missing {
fmt.Fprintf(stderr, " - %s\n", v)
}
fmt.Fprintln(stderr, "\nEither:\n - Set those environment variables before running cagent\n - Run cagent with --env-from-file\n - Store those secrets using one of the built-in environment variable providers.")
} else if _, ok := errors.AsType[RuntimeError](err); ok {
// Runtime errors have already been printed by the command itself
// Don't print them again or show usage
} else {
// Command line usage errors - show the error and usage
fmt.Fprintln(stderr, err)
fmt.Fprintln(stderr)
if strings.HasPrefix(err.Error(), "unknown command ") || strings.HasPrefix(err.Error(), "accepts ") {
_ = rootCmd.Usage()
if isCliPLugin() {
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MEDIUM SEVERITY: Context not propagated to plugin.Run()

The ctx parameter passed to Execute() is not used in the plugin path. In the non-plugin path (line 186), rootCmd.ExecuteContext(ctx) properly propagates the context. However, in the plugin path, the context is never passed to or used by plugin.Run().

Impact: Context cancellation, timeouts, and other context-based operations won't work when running as a CLI plugin. This creates inconsistent behavior between plugin and non-plugin execution modes.

Suggested fix: Consider calling rootCmd.SetContext(ctx) before passing it to the plugin callback, or investigate if the Docker CLI plugin framework provides a way to pass context.

originalPreRun := rootCmd.PersistentPreRunE
rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
return err
}
if originalPreRun != nil {
if err := originalPreRun(cmd, args); err != nil {
return processErr(ctx, err, stderr, rootCmd)
}
}
return nil
}
rootCmd.SetContext(ctx)
return rootCmd
}, metadata.Metadata{
SchemaVersion: "0.1.0",
Vendor: "Docker Inc.",
Version: version.Version,
})
} else {
if err := rootCmd.ExecuteContext(ctx); err != nil {
return processErr(ctx, err, stderr, rootCmd)
}

return err
}

return nil
}

func processErr(ctx context.Context, err error, stderr io.Writer, rootCmd *cobra.Command) error {
if ctx.Err() != nil {
return ctx.Err()
} else if envErr, ok := errors.AsType[*environment.RequiredEnvError](err); ok {
fmt.Fprintln(stderr, "The following environment variables must be set:")
for _, v := range envErr.Missing {
fmt.Fprintf(stderr, " - %s\n", v)
}
fmt.Fprintln(stderr, "\nEither:\n - Set those environment variables before running cagent\n - Run cagent with --env-from-file\n - Store those secrets using one of the built-in environment variable providers.")
} else if _, ok := errors.AsType[RuntimeError](err); ok {
// Runtime errors have already been printed by the command itself
// Don't print them again or show usage
} else {
// Command line usage errors - show the error and usage
fmt.Fprintln(stderr, err)
fmt.Fprintln(stderr)
if strings.HasPrefix(err.Error(), "unknown command ") || strings.HasPrefix(err.Error(), "accepts ") {
_ = rootCmd.Usage()
}
}

return err
}

// setupLogging configures slog logging behavior.
// When --debug is enabled, logs are written to a rotating file <dataDir>/cagent.debug.log,
// or to the file specified by --log-file. Log files are rotated when they exceed 10MB,
Expand Down
23 changes: 21 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ require (
github.com/clipperhouse/displaywidth v0.10.0
github.com/clipperhouse/uax29/v2 v2.6.0
github.com/coder/acp-go-sdk v0.6.3
github.com/docker/cli v29.0.3+incompatible
github.com/docker/go-units v0.5.0
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
github.com/fatih/color v1.18.0
Expand Down Expand Up @@ -79,6 +80,7 @@ require (
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
dario.cat/mergo v1.0.2 // indirect
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
github.com/JohannesKaufmann/dom v0.2.0 // indirect
github.com/ProtonMail/go-crypto v1.1.6 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
Expand Down Expand Up @@ -123,16 +125,21 @@ require (
github.com/charmbracelet/x/termios v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.2.2 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/containerd/errdefs v1.0.0 // indirect
github.com/containerd/errdefs/pkg v0.3.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/dlclark/regexp2 v1.11.5 // indirect
github.com/docker/cli v29.0.3+incompatible // indirect
github.com/docker/distribution v2.8.3+incompatible // indirect
github.com/docker/docker-credential-helpers v0.9.3 // indirect
github.com/docker/go-connections v0.6.0 // indirect
github.com/docker/go-metrics v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fvbommel/sortorder v1.1.0 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-git/go-billy/v5 v5.6.2 // indirect
github.com/go-logr/logr v1.4.3 // indirect
Expand All @@ -153,7 +160,7 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v0.0.0-20171115153421-f7279a603ede // indirect
github.com/json-iterator/go v1.1.7 // indirect
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/labstack/gommon v0.4.2 // indirect
Expand All @@ -162,7 +169,16 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/moby/api v1.53.0 // indirect
github.com/moby/moby/client v0.2.2 // indirect
github.com/moby/sys/atomicwriter v0.1.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect
github.com/moby/term v0.5.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/mschoch/smat v0.2.0 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
Expand Down Expand Up @@ -195,8 +211,11 @@ require (
go.etcd.io/bbolt v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect
golang.org/x/crypto v0.48.0 // indirect
Expand Down
Loading